This commit is contained in:
2025-09-17 14:40:16 +08:00
parent 46c5682960
commit a3bbc0098a
94 changed files with 3549 additions and 105 deletions

View File

@@ -1,6 +1,6 @@
## 前后端数据库状态说明
**更新日期**: 2025-09-16
**更新日期**: 2025-09-17
### 概要
- 数据库已落地:已在远程 MySQL `mysql.tonaspace.com``partsinquiry` 库完成初始化(表结构与触发器已创建)。
@@ -27,12 +27,15 @@
- 关闭:不设置/置为 `false` 即可停用(生产环境默认关闭)。
- 完全移除:删除 `frontend/common/config.js` 中默认用户配置与 `frontend/common/http.js` 中注入逻辑。
### 后端Spring Boot数据库状态
- 依赖:`pom.xml` 包含 `spring-boot-starter-web``spring-boot-starter-data-jpa``mysql-connector-j` 等数据库相关依赖
- 配置:`src/main/resources/application.properties` 仅有 `spring.application.name=demo`;未配置 `spring.datasource.*``spring.jpa.*`
- 数据模型:`src/main/java` 未发现 `@Entity`、Repository、Service存在 `backend/db/db.sql` 脚本,已执行至远程库。
- 迁移:未发现 Flyway/Liquibase 配置与脚本(当前通过 MysqlMCP 手工执行)。
- 结论:数据库已初始化,但后端未配置运行时数据源与接口,暂不可用。
### 后端Spring Boot状态
- 依赖:`pom.xml` 包含 `spring-boot-starter-web``spring-boot-starter-data-jpa``mysql-connector-j`
- 配置:`application.properties` 使用环境变量注入数据源,已补充 Hikari/JPA新增附件占位图配置
- `attachments.placeholder.image-path`env: `ATTACHMENTS_PLACEHOLDER_IMAGE`
- `attachments.placeholder.url-path`env: `ATTACHMENTS_PLACEHOLDER_URL`,默认 `/api/attachments/placeholder`
- 接口:新增附件相关接口(占位方案):
- POST `/api/attachments`:忽略内容,返回 `{ url: "/api/attachments/placeholder" }`
- GET `/api/attachments/placeholder`:返回本地占位图二进制
- 迁移:仍建议引入 Flyway/Liquibase结构变更继续通过 MysqlMCP 并同步 `/doc/database_documentation.md`
### 前端uni-app数据库状态
- 数据持久化:未见 IndexedDB/WebSQL/SQLite/云数据库使用;页面数据为内置静态数据。
@@ -49,5 +52,26 @@
- 引入迁移工具Flyway/Liquibase管理 DDL后续所有变更继续通过 MysqlMCP 执行,并同步 `/doc/database_documentation.md`
- 增加健康检查与基础 CRUD 接口;在 `/doc/openapi.yaml` 按规范登记并标注实现状态(❌/✅)。
### 前端默认连接策略
- 默认后端地址:`http://192.168.31.193:8080`(可被环境变量/Storage 覆盖)
- 多地址重试:按顺序尝试(去重处理):`[ENV, Storage, 192.168.31.193:8080, 127.0.0.1:8080, localhost:8080]`
- 默认用户:开启(可被环境变量/Storage 关闭),请求自动附带 `X-User-Id`(默认 `2`)。
- 如需关闭:在 Storage 或构建环境中设置 `ENABLE_DEFAULT_USER=false`
### 占位图策略(当前阶段)
- 说明:所有图片上传与展示均统一使用占位图,实际文件存储暂不开发。
- 本地占位图:`C:\Users\21826\Desktop\Wj\PartsInquiry\backend\picture\屏幕截图 2025-08-14 134657.png`
- 配置方式:
- PowerShell当前用户持久化
```powershell
setx ATTACHMENTS_PLACEHOLDER_IMAGE "C:\\Users\\21826\\Desktop\\Wj\\PartsInquiry\\backend\\picture\\屏幕截图 2025-08-14 134657.png"
setx ATTACHMENTS_PLACEHOLDER_URL "/api/attachments/placeholder"
```
- 应用重启后生效;也可在运行环境变量中注入。
- 前端影响:
- `components/ImageUploader.vue` 上传始终得到 `{ url: '/api/attachments/placeholder' }`
- 商品列表/详情展示该占位图地址

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View File

@@ -0,0 +1,73 @@
package com.example.demo.attachment;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import org.springframework.http.CacheControl;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
@RestController
@RequestMapping("/api/attachments")
public class AttachmentController {
private final AttachmentPlaceholderProperties placeholderProperties;
public AttachmentController(AttachmentPlaceholderProperties placeholderProperties) {
this.placeholderProperties = placeholderProperties;
}
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity<Map<String, Object>> upload(@RequestParam("file") MultipartFile file,
@RequestParam(value = "ownerType", required = false) String ownerType,
@RequestParam(value = "ownerId", required = false) String ownerId) {
// 占位实现:忽略文件内容,始终返回占位图 URL
String url = StringUtils.hasText(placeholderProperties.getUrlPath()) ? placeholderProperties.getUrlPath() : "/api/attachments/placeholder";
Map<String, Object> body = new HashMap<>();
body.put("url", url);
return ResponseEntity.ok(body);
}
@GetMapping("/placeholder")
public ResponseEntity<Resource> placeholder() throws IOException {
String imagePath = placeholderProperties.getImagePath();
if (!StringUtils.hasText(imagePath)) {
return ResponseEntity.status(404).build();
}
Path path = Path.of(imagePath);
if (!Files.exists(path)) {
return ResponseEntity.status(404).build();
}
Resource resource = new FileSystemResource(path);
String contentType = Files.probeContentType(path);
MediaType mediaType;
try {
mediaType = StringUtils.hasText(contentType) ? MediaType.parseMediaType(contentType) : MediaType.IMAGE_PNG;
} catch (Exception e) {
mediaType = MediaType.APPLICATION_OCTET_STREAM;
}
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=placeholder")
.cacheControl(CacheControl.maxAge(30, TimeUnit.DAYS).cachePublic())
.contentType(mediaType)
.body(resource);
}
}

View File

@@ -0,0 +1,32 @@
package com.example.demo.attachment;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Component
@ConfigurationProperties(prefix = "attachments.placeholder")
public class AttachmentPlaceholderProperties {
private String imagePath;
private String urlPath = "/api/attachments/placeholder";
public String getImagePath() {
return imagePath;
}
public void setImagePath(String imagePath) {
this.imagePath = imagePath;
}
public String getUrlPath() {
return urlPath;
}
public void setUrlPath(String urlPath) {
this.urlPath = urlPath;
}
}

View File

@@ -0,0 +1,21 @@
package com.example.demo.common;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Component
@ConfigurationProperties(prefix = "app.defaults")
public class AppDefaultsProperties {
private Long shopId = 1L;
private Long userId = 2L;
public Long getShopId() { return shopId; }
public void setShopId(Long shopId) { this.shopId = shopId; }
public Long getUserId() { return userId; }
public void setUserId(Long userId) { this.userId = userId; }
}

View File

@@ -17,9 +17,11 @@ public class DashboardController {
}
@GetMapping("/overview")
public ResponseEntity<DashboardOverviewResponse> overview(@RequestHeader(name = "X-Shop-Id", required = false) Long shopId) {
long sid = (shopId == null ? 1L : shopId);
return ResponseEntity.ok(dashboardService.getOverview(sid));
public ResponseEntity<DashboardOverviewResponse> overview(
@RequestHeader(name = "X-Shop-Id", required = false) Long shopId,
@RequestHeader(name = "X-User-Id", required = false) Long userId
) {
return ResponseEntity.ok(dashboardService.getOverviewByUserOrShop(userId, shopId));
}
}

View File

@@ -4,11 +4,13 @@ import java.math.BigDecimal;
public class DashboardOverviewResponse {
private BigDecimal todaySalesAmount;
private BigDecimal monthSalesAmount;
private BigDecimal monthGrossProfit;
private BigDecimal stockTotalQuantity;
public DashboardOverviewResponse(BigDecimal todaySalesAmount, BigDecimal monthGrossProfit, BigDecimal stockTotalQuantity) {
public DashboardOverviewResponse(BigDecimal todaySalesAmount, BigDecimal monthSalesAmount, BigDecimal monthGrossProfit, BigDecimal stockTotalQuantity) {
this.todaySalesAmount = todaySalesAmount;
this.monthSalesAmount = monthSalesAmount;
this.monthGrossProfit = monthGrossProfit;
this.stockTotalQuantity = stockTotalQuantity;
}
@@ -24,6 +26,10 @@ public class DashboardOverviewResponse {
public BigDecimal getStockTotalQuantity() {
return stockTotalQuantity;
}
public BigDecimal getMonthSalesAmount() {
return monthSalesAmount;
}
}

View File

@@ -33,6 +33,16 @@ public class DashboardRepository {
return toBigDecimal(result);
}
public BigDecimal sumMonthSalesOrders(Long shopId) {
Object result = entityManager.createNativeQuery(
"SELECT COALESCE(SUM(amount), 0) FROM sales_orders " +
"WHERE shop_id = :shopId AND status = 'approved' " +
"AND order_time >= DATE_FORMAT(CURRENT_DATE(), '%Y-%m-01') " +
"AND order_time < DATE_ADD(DATE_FORMAT(CURRENT_DATE(), '%Y-%m-01'), INTERVAL 1 MONTH)"
).setParameter("shopId", shopId).getSingleResult();
return toBigDecimal(result);
}
public BigDecimal sumTotalInventoryQty(Long shopId) {
Object result = entityManager.createNativeQuery(
"SELECT COALESCE(SUM(quantity), 0) FROM inventories WHERE shop_id = :shopId"
@@ -50,6 +60,20 @@ public class DashboardRepository {
return BigDecimal.ZERO;
}
}
public Long findShopIdByUserId(Long userId) {
if (userId == null) return null;
Object result = entityManager.createNativeQuery(
"SELECT shop_id FROM users WHERE id = :userId LIMIT 1"
).setParameter("userId", userId).getSingleResult();
if (result == null) return null;
if (result instanceof Number) return ((Number) result).longValue();
try {
return Long.valueOf(result.toString());
} catch (Exception e) {
return null;
}
}
}

View File

@@ -14,9 +14,24 @@ public class DashboardService {
public DashboardOverviewResponse getOverview(long shopId) {
BigDecimal todaySales = dashboardRepository.sumTodaySalesOrders(shopId);
BigDecimal monthSales = dashboardRepository.sumMonthSalesOrders(shopId);
BigDecimal monthGrossProfit = dashboardRepository.sumMonthGrossProfitApprox(shopId);
BigDecimal stockTotalQty = dashboardRepository.sumTotalInventoryQty(shopId);
return new DashboardOverviewResponse(todaySales, monthGrossProfit, stockTotalQty);
return new DashboardOverviewResponse(todaySales, monthSales, monthGrossProfit, stockTotalQty);
}
public DashboardOverviewResponse getOverviewByUserOrShop(Long userId, Long shopIdOrNull) {
Long resolvedShopId = null;
if (userId != null) {
resolvedShopId = dashboardRepository.findShopIdByUserId(userId);
}
if (resolvedShopId == null && shopIdOrNull != null) {
resolvedShopId = shopIdOrNull;
}
if (resolvedShopId == null) {
resolvedShopId = 1L;
}
return getOverview(resolvedShopId);
}
}

View File

@@ -0,0 +1,44 @@
package com.example.demo.product.controller;
import com.example.demo.common.AppDefaultsProperties;
import com.example.demo.product.repo.CategoryRepository;
import com.example.demo.product.repo.UnitRepository;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
@RestController
public class MetadataController {
private final UnitRepository unitRepository;
private final CategoryRepository categoryRepository;
private final AppDefaultsProperties defaults;
public MetadataController(UnitRepository unitRepository, CategoryRepository categoryRepository, AppDefaultsProperties defaults) {
this.unitRepository = unitRepository;
this.categoryRepository = categoryRepository;
this.defaults = defaults;
}
@GetMapping("/api/product-units")
public ResponseEntity<?> listUnits(@RequestHeader(name = "X-Shop-Id", required = false) Long shopId) {
Long sid = (shopId == null ? defaults.getShopId() : shopId);
Map<String, Object> body = new HashMap<>();
body.put("list", unitRepository.listByShop(sid));
return ResponseEntity.ok(body);
}
@GetMapping("/api/product-categories")
public ResponseEntity<?> listCategories(@RequestHeader(name = "X-Shop-Id", required = false) Long shopId) {
Long sid = (shopId == null ? defaults.getShopId() : shopId);
Map<String, Object> body = new HashMap<>();
body.put("list", categoryRepository.listByShop(sid));
return ResponseEntity.ok(body);
}
}

View File

@@ -0,0 +1,66 @@
package com.example.demo.product.controller;
import com.example.demo.common.AppDefaultsProperties;
import com.example.demo.product.dto.ProductDtos;
import com.example.demo.product.service.ProductService;
import org.springframework.data.domain.Page;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/products")
public class ProductController {
private final ProductService productService;
private final AppDefaultsProperties defaults;
public ProductController(ProductService productService, AppDefaultsProperties defaults) {
this.productService = productService;
this.defaults = defaults;
}
@GetMapping
public ResponseEntity<?> search(@RequestHeader(name = "X-Shop-Id", required = false) Long shopId,
@RequestParam(name = "kw", required = false) String kw,
@RequestParam(name = "categoryId", required = false) Long categoryId,
@RequestParam(name = "page", defaultValue = "1") int page,
@RequestParam(name = "size", defaultValue = "50") int size) {
Long sid = (shopId == null ? defaults.getShopId() : shopId);
Page<ProductDtos.ProductListItem> result = productService.search(sid, kw, categoryId, Math.max(page - 1, 0), size);
java.util.Map<String, Object> body = new java.util.HashMap<>();
body.put("list", result.getContent());
return ResponseEntity.ok(body);
}
@GetMapping("/{id}")
public ResponseEntity<?> detail(@PathVariable("id") Long id) {
return productService.findDetail(id)
.<ResponseEntity<?>>map(ResponseEntity::ok)
.orElseGet(() -> ResponseEntity.notFound().build());
}
@PostMapping
public ResponseEntity<?> create(@RequestHeader(name = "X-Shop-Id", required = false) Long shopId,
@RequestHeader(name = "X-User-Id", required = false) Long userId,
@RequestBody ProductDtos.CreateOrUpdateProductRequest req) {
Long sid = (shopId == null ? defaults.getShopId() : shopId);
Long uid = (userId == null ? defaults.getUserId() : userId);
Long id = productService.create(sid, uid, req);
java.util.Map<String, Object> body = new java.util.HashMap<>();
body.put("id", id);
return ResponseEntity.ok(body);
}
@PutMapping("/{id}")
public ResponseEntity<?> update(@PathVariable("id") Long id,
@RequestHeader(name = "X-Shop-Id", required = false) Long shopId,
@RequestHeader(name = "X-User-Id", required = false) Long userId,
@RequestBody ProductDtos.CreateOrUpdateProductRequest req) {
Long sid = (shopId == null ? defaults.getShopId() : shopId);
Long uid = (userId == null ? defaults.getUserId() : userId);
productService.update(id, sid, uid, req);
return ResponseEntity.ok().build();
}
}

View File

@@ -0,0 +1,72 @@
package com.example.demo.product.dto;
import java.math.BigDecimal;
import java.util.List;
public class ProductDtos {
public static class ProductListItem {
public Long id;
public String name;
public String brand;
public String model;
public String spec;
public BigDecimal stock; // from inventories.quantity
public BigDecimal retailPrice; // from product_prices
public String cover; // first image url
}
public static class ProductDetail {
public Long id;
public String name;
public String barcode;
public String brand;
public String model;
public String spec;
public String origin;
public Long categoryId;
public Long unitId;
public BigDecimal safeMin;
public BigDecimal safeMax;
public BigDecimal stock;
public BigDecimal purchasePrice;
public BigDecimal retailPrice;
public BigDecimal distributionPrice;
public BigDecimal wholesalePrice;
public BigDecimal bigClientPrice;
public List<Image> images;
}
public static class Image {
public String url;
}
public static class CreateOrUpdateProductRequest {
public String name;
public String barcode;
public String brand;
public String model;
public String spec;
public String origin;
public Long categoryId;
public Long unitId;
public BigDecimal safeMin;
public BigDecimal safeMax;
public Prices prices;
public BigDecimal stock;
public List<String> images;
public String remark; // map to products.description
}
public static class Prices {
public BigDecimal purchasePrice;
public BigDecimal retailPrice;
public BigDecimal distributionPrice;
public BigDecimal wholesalePrice;
public BigDecimal bigClientPrice;
}
}

View File

@@ -0,0 +1,41 @@
package com.example.demo.product.entity;
import jakarta.persistence.*;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Entity
@Table(name = "inventories")
public class Inventory {
@Id
@Column(name = "product_id")
private Long productId;
@Column(name = "shop_id", nullable = false)
private Long shopId;
@Column(name = "user_id", nullable = false)
private Long userId;
@Column(name = "quantity", precision = 18, scale = 3, nullable = false)
private BigDecimal quantity = BigDecimal.ZERO;
@Column(name = "updated_at", nullable = false)
private LocalDateTime updatedAt;
public Long getProductId() { return productId; }
public void setProductId(Long productId) { this.productId = productId; }
public Long getShopId() { return shopId; }
public void setShopId(Long shopId) { this.shopId = shopId; }
public Long getUserId() { return userId; }
public void setUserId(Long userId) { this.userId = userId; }
public BigDecimal getQuantity() { return quantity; }
public void setQuantity(BigDecimal quantity) { this.quantity = quantity; }
public LocalDateTime getUpdatedAt() { return updatedAt; }
public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
}

View File

@@ -0,0 +1,100 @@
package com.example.demo.product.entity;
import jakarta.persistence.*;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Entity
@Table(name = "products")
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "shop_id", nullable = false)
private Long shopId;
@Column(name = "user_id", nullable = false)
private Long userId;
@Column(name = "name", nullable = false, length = 120)
private String name;
@Column(name = "category_id")
private Long categoryId;
@Column(name = "unit_id", nullable = false)
private Long unitId;
@Column(name = "brand", length = 64)
private String brand;
@Column(name = "model", length = 64)
private String model;
@Column(name = "spec", length = 128)
private String spec;
@Column(name = "origin", length = 64)
private String origin;
@Column(name = "barcode", length = 32)
private String barcode;
@Column(name = "description")
private String description;
@Column(name = "safe_min", precision = 18, scale = 3)
private BigDecimal safeMin;
@Column(name = "safe_max", precision = 18, scale = 3)
private BigDecimal safeMax;
@Column(name = "created_at")
private LocalDateTime createdAt;
@Column(name = "updated_at")
private LocalDateTime updatedAt;
@Column(name = "deleted_at")
private LocalDateTime deletedAt;
public Long getId() { return id; }
public Long getShopId() { return shopId; }
public void setShopId(Long shopId) { this.shopId = shopId; }
public Long getUserId() { return userId; }
public void setUserId(Long userId) { this.userId = userId; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public Long getCategoryId() { return categoryId; }
public void setCategoryId(Long categoryId) { this.categoryId = categoryId; }
public Long getUnitId() { return unitId; }
public void setUnitId(Long unitId) { this.unitId = unitId; }
public String getBrand() { return brand; }
public void setBrand(String brand) { this.brand = brand; }
public String getModel() { return model; }
public void setModel(String model) { this.model = model; }
public String getSpec() { return spec; }
public void setSpec(String spec) { this.spec = spec; }
public String getOrigin() { return origin; }
public void setOrigin(String origin) { this.origin = origin; }
public String getBarcode() { return barcode; }
public void setBarcode(String barcode) { this.barcode = barcode; }
public String getDescription() { return description; }
public void setDescription(String description) { this.description = description; }
public BigDecimal getSafeMin() { return safeMin; }
public void setSafeMin(BigDecimal safeMin) { this.safeMin = safeMin; }
public BigDecimal getSafeMax() { return safeMax; }
public void setSafeMax(BigDecimal safeMax) { this.safeMax = safeMax; }
public LocalDateTime getCreatedAt() { return createdAt; }
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
public LocalDateTime getUpdatedAt() { return updatedAt; }
public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
public LocalDateTime getDeletedAt() { return deletedAt; }
public void setDeletedAt(LocalDateTime deletedAt) { this.deletedAt = deletedAt; }
}

View File

@@ -0,0 +1,58 @@
package com.example.demo.product.entity;
import jakarta.persistence.*;
import java.time.LocalDateTime;
@Entity
@Table(name = "product_categories")
public class ProductCategory {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "shop_id", nullable = false)
private Long shopId;
@Column(name = "user_id", nullable = false)
private Long userId;
@Column(name = "name", nullable = false, length = 64)
private String name;
@Column(name = "parent_id")
private Long parentId;
@Column(name = "sort_order", nullable = false)
private Integer sortOrder = 0;
@Column(name = "created_at")
private LocalDateTime createdAt;
@Column(name = "updated_at")
private LocalDateTime updatedAt;
@Column(name = "deleted_at")
private LocalDateTime deletedAt;
public Long getId() { return id; }
public Long getShopId() { return shopId; }
public void setShopId(Long shopId) { this.shopId = shopId; }
public Long getUserId() { return userId; }
public void setUserId(Long userId) { this.userId = userId; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public Long getParentId() { return parentId; }
public void setParentId(Long parentId) { this.parentId = parentId; }
public Integer getSortOrder() { return sortOrder; }
public void setSortOrder(Integer sortOrder) { this.sortOrder = sortOrder; }
public LocalDateTime getCreatedAt() { return createdAt; }
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
public LocalDateTime getUpdatedAt() { return updatedAt; }
public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
public LocalDateTime getDeletedAt() { return deletedAt; }
public void setDeletedAt(LocalDateTime deletedAt) { this.deletedAt = deletedAt; }
}

View File

@@ -0,0 +1,43 @@
package com.example.demo.product.entity;
import jakarta.persistence.*;
@Entity
@Table(name = "product_images")
public class ProductImage {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "shop_id", nullable = false)
private Long shopId;
@Column(name = "user_id", nullable = false)
private Long userId;
@Column(name = "product_id", nullable = false)
private Long productId;
@Column(name = "url", nullable = false, length = 512)
private String url;
@Column(name = "sort_order", nullable = false)
private Integer sortOrder = 0;
public Long getId() { return id; }
public Long getShopId() { return shopId; }
public void setShopId(Long shopId) { this.shopId = shopId; }
public Long getUserId() { return userId; }
public void setUserId(Long userId) { this.userId = userId; }
public Long getProductId() { return productId; }
public void setProductId(Long productId) { this.productId = productId; }
public String getUrl() { return url; }
public void setUrl(String url) { this.url = url; }
public Integer getSortOrder() { return sortOrder; }
public void setSortOrder(Integer sortOrder) { this.sortOrder = sortOrder; }
}

View File

@@ -0,0 +1,61 @@
package com.example.demo.product.entity;
import jakarta.persistence.*;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Entity
@Table(name = "product_prices")
public class ProductPrice {
@Id
@Column(name = "product_id")
private Long productId;
@Column(name = "shop_id", nullable = false)
private Long shopId;
@Column(name = "user_id", nullable = false)
private Long userId;
@Column(name = "purchase_price", precision = 18, scale = 2, nullable = false)
private BigDecimal purchasePrice = BigDecimal.ZERO;
@Column(name = "retail_price", precision = 18, scale = 2, nullable = false)
private BigDecimal retailPrice = BigDecimal.ZERO;
@Column(name = "distribution_price", precision = 18, scale = 2, nullable = false)
private BigDecimal distributionPrice = BigDecimal.ZERO;
@Column(name = "wholesale_price", precision = 18, scale = 2, nullable = false)
private BigDecimal wholesalePrice = BigDecimal.ZERO;
@Column(name = "big_client_price", precision = 18, scale = 2, nullable = false)
private BigDecimal bigClientPrice = BigDecimal.ZERO;
@Column(name = "updated_at", nullable = false)
private LocalDateTime updatedAt;
public Long getProductId() { return productId; }
public void setProductId(Long productId) { this.productId = productId; }
public Long getShopId() { return shopId; }
public void setShopId(Long shopId) { this.shopId = shopId; }
public Long getUserId() { return userId; }
public void setUserId(Long userId) { this.userId = userId; }
public BigDecimal getPurchasePrice() { return purchasePrice; }
public void setPurchasePrice(BigDecimal purchasePrice) { this.purchasePrice = purchasePrice; }
public BigDecimal getRetailPrice() { return retailPrice; }
public void setRetailPrice(BigDecimal retailPrice) { this.retailPrice = retailPrice; }
public BigDecimal getDistributionPrice() { return distributionPrice; }
public void setDistributionPrice(BigDecimal distributionPrice) { this.distributionPrice = distributionPrice; }
public BigDecimal getWholesalePrice() { return wholesalePrice; }
public void setWholesalePrice(BigDecimal wholesalePrice) { this.wholesalePrice = wholesalePrice; }
public BigDecimal getBigClientPrice() { return bigClientPrice; }
public void setBigClientPrice(BigDecimal bigClientPrice) { this.bigClientPrice = bigClientPrice; }
public LocalDateTime getUpdatedAt() { return updatedAt; }
public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
}

View File

@@ -0,0 +1,48 @@
package com.example.demo.product.entity;
import jakarta.persistence.*;
import java.time.LocalDateTime;
@Entity
@Table(name = "product_units")
public class ProductUnit {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "shop_id", nullable = false)
private Long shopId;
@Column(name = "user_id", nullable = false)
private Long userId;
@Column(name = "name", nullable = false, length = 16)
private String name;
@Column(name = "created_at")
private LocalDateTime createdAt;
@Column(name = "updated_at")
private LocalDateTime updatedAt;
@Column(name = "deleted_at")
private LocalDateTime deletedAt;
public Long getId() { return id; }
public Long getShopId() { return shopId; }
public void setShopId(Long shopId) { this.shopId = shopId; }
public Long getUserId() { return userId; }
public void setUserId(Long userId) { this.userId = userId; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public LocalDateTime getCreatedAt() { return createdAt; }
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
public LocalDateTime getUpdatedAt() { return updatedAt; }
public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
public LocalDateTime getDeletedAt() { return deletedAt; }
public void setDeletedAt(LocalDateTime deletedAt) { this.deletedAt = deletedAt; }
}

View File

@@ -0,0 +1,17 @@
package com.example.demo.product.repo;
import com.example.demo.product.entity.ProductCategory;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import java.util.List;
public interface CategoryRepository extends JpaRepository<ProductCategory, Long> {
@Query("SELECT c FROM ProductCategory c WHERE c.shopId = :shopId AND c.deletedAt IS NULL ORDER BY c.sortOrder ASC, c.id DESC")
List<ProductCategory> listByShop(@Param("shopId") Long shopId);
}

View File

@@ -0,0 +1,11 @@
package com.example.demo.product.repo;
import com.example.demo.product.entity.Inventory;
import org.springframework.data.jpa.repository.JpaRepository;
public interface InventoryRepository extends JpaRepository<Inventory, Long> {
}

View File

@@ -0,0 +1,15 @@
package com.example.demo.product.repo;
import com.example.demo.product.entity.ProductImage;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
public interface ProductImageRepository extends JpaRepository<ProductImage, Long> {
List<ProductImage> findByProductIdOrderBySortOrderAscIdAsc(Long productId);
void deleteByProductId(Long productId);
}

View File

@@ -0,0 +1,11 @@
package com.example.demo.product.repo;
import com.example.demo.product.entity.ProductPrice;
import org.springframework.data.jpa.repository.JpaRepository;
public interface ProductPriceRepository extends JpaRepository<ProductPrice, Long> {
}

View File

@@ -0,0 +1,25 @@
package com.example.demo.product.repo;
import com.example.demo.product.entity.Product;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
public interface ProductRepository extends JpaRepository<Product, Long> {
@Query("SELECT p FROM Product p WHERE p.shopId = :shopId AND (p.deletedAt IS NULL) AND " +
"(:kw IS NULL OR :kw = '' OR p.name LIKE CONCAT('%', :kw, '%') OR p.brand LIKE CONCAT('%', :kw, '%') OR p.model LIKE CONCAT('%', :kw, '%') OR p.spec LIKE CONCAT('%', :kw, '%') OR p.barcode LIKE CONCAT('%', :kw, '%')) AND " +
"(:categoryId IS NULL OR p.categoryId = :categoryId) ORDER BY p.id DESC")
Page<Product> search(@Param("shopId") Long shopId,
@Param("kw") String kw,
@Param("categoryId") Long categoryId,
Pageable pageable);
boolean existsByShopIdAndBarcode(Long shopId, String barcode);
}

View File

@@ -0,0 +1,16 @@
package com.example.demo.product.repo;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import java.util.List;
public interface UnitRepository extends JpaRepository<com.example.demo.product.entity.ProductUnit, Long> {
@Query("SELECT u FROM ProductUnit u WHERE u.shopId = :shopId AND u.deletedAt IS NULL ORDER BY u.id DESC")
List<com.example.demo.product.entity.ProductUnit> listByShop(@Param("shopId") Long shopId);
}

View File

@@ -0,0 +1,216 @@
package com.example.demo.product.service;
import com.example.demo.product.dto.ProductDtos;
import com.example.demo.product.entity.Inventory;
import com.example.demo.product.entity.Product;
import com.example.demo.product.entity.ProductImage;
import com.example.demo.product.entity.ProductPrice;
import com.example.demo.product.repo.InventoryRepository;
import com.example.demo.product.repo.ProductImageRepository;
import com.example.demo.product.repo.ProductPriceRepository;
import com.example.demo.product.repo.ProductRepository;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
@Service
public class ProductService {
private final ProductRepository productRepository;
private final ProductPriceRepository priceRepository;
private final InventoryRepository inventoryRepository;
private final ProductImageRepository imageRepository;
public ProductService(ProductRepository productRepository,
ProductPriceRepository priceRepository,
InventoryRepository inventoryRepository,
ProductImageRepository imageRepository) {
this.productRepository = productRepository;
this.priceRepository = priceRepository;
this.inventoryRepository = inventoryRepository;
this.imageRepository = imageRepository;
}
public Page<ProductDtos.ProductListItem> search(Long shopId, String kw, Long categoryId, int page, int size) {
Page<Product> p = productRepository.search(shopId, kw, categoryId, PageRequest.of(page, size));
return p.map(prod -> {
ProductDtos.ProductListItem it = new ProductDtos.ProductListItem();
it.id = prod.getId();
it.name = prod.getName();
it.brand = prod.getBrand();
it.model = prod.getModel();
it.spec = prod.getSpec();
// stock
inventoryRepository.findById(prod.getId()).ifPresent(inv -> it.stock = inv.getQuantity());
// price
priceRepository.findById(prod.getId()).ifPresent(pr -> it.retailPrice = pr.getRetailPrice());
// cover
List<ProductImage> imgs = imageRepository.findByProductIdOrderBySortOrderAscIdAsc(prod.getId());
it.cover = imgs.isEmpty() ? null : imgs.get(0).getUrl();
return it;
});
}
public Optional<ProductDtos.ProductDetail> findDetail(Long id) {
Optional<Product> op = productRepository.findById(id);
if (op.isEmpty()) return Optional.empty();
Product p = op.get();
ProductDtos.ProductDetail d = new ProductDtos.ProductDetail();
d.id = p.getId();
d.name = p.getName();
d.barcode = p.getBarcode();
d.brand = p.getBrand();
d.model = p.getModel();
d.spec = p.getSpec();
d.origin = p.getOrigin();
d.categoryId = p.getCategoryId();
d.unitId = p.getUnitId();
d.safeMin = p.getSafeMin();
d.safeMax = p.getSafeMax();
inventoryRepository.findById(p.getId()).ifPresent(inv -> d.stock = inv.getQuantity());
priceRepository.findById(p.getId()).ifPresent(pr -> {
d.purchasePrice = pr.getPurchasePrice();
d.retailPrice = pr.getRetailPrice();
d.distributionPrice = pr.getDistributionPrice();
d.wholesalePrice = pr.getWholesalePrice();
d.bigClientPrice = pr.getBigClientPrice();
});
List<ProductImage> imgs = imageRepository.findByProductIdOrderBySortOrderAscIdAsc(p.getId());
List<ProductDtos.Image> list = new ArrayList<>();
for (ProductImage img : imgs) {
ProductDtos.Image i = new ProductDtos.Image();
i.url = img.getUrl();
list.add(i);
}
d.images = list;
return Optional.of(d);
}
@Transactional
public Long create(Long shopId, Long userId, ProductDtos.CreateOrUpdateProductRequest req) {
validate(shopId, req);
LocalDateTime now = LocalDateTime.now();
Product p = new Product();
p.setShopId(shopId);
p.setUserId(userId);
p.setName(req.name);
p.setBarcode(emptyToNull(req.barcode));
p.setBrand(emptyToNull(req.brand));
p.setModel(emptyToNull(req.model));
p.setSpec(emptyToNull(req.spec));
p.setOrigin(emptyToNull(req.origin));
p.setCategoryId(req.categoryId);
p.setUnitId(req.unitId);
p.setSafeMin(req.safeMin);
p.setSafeMax(req.safeMax);
p.setDescription(emptyToNull(req.remark));
p.setCreatedAt(now);
p.setUpdatedAt(now);
productRepository.save(p);
upsertPrice(userId, now, p.getId(), shopId, req.prices);
upsertInventory(userId, now, p.getId(), shopId, req.stock);
syncImages(userId, p.getId(), shopId, req.images);
return p.getId();
}
@Transactional
public void update(Long id, Long shopId, Long userId, ProductDtos.CreateOrUpdateProductRequest req) {
validate(shopId, req);
Product p = productRepository.findById(id).orElseThrow();
if (!p.getShopId().equals(shopId)) throw new IllegalArgumentException("跨店铺数据");
p.setUserId(userId);
p.setName(req.name);
p.setBarcode(emptyToNull(req.barcode));
p.setBrand(emptyToNull(req.brand));
p.setModel(emptyToNull(req.model));
p.setSpec(emptyToNull(req.spec));
p.setOrigin(emptyToNull(req.origin));
p.setCategoryId(req.categoryId);
p.setUnitId(req.unitId);
p.setSafeMin(req.safeMin);
p.setSafeMax(req.safeMax);
p.setDescription(emptyToNull(req.remark));
p.setUpdatedAt(LocalDateTime.now());
productRepository.save(p);
LocalDateTime now = LocalDateTime.now();
upsertPrice(userId, now, p.getId(), shopId, req.prices);
upsertInventory(userId, now, p.getId(), shopId, req.stock);
syncImages(userId, p.getId(), shopId, req.images);
}
private void validate(Long shopId, ProductDtos.CreateOrUpdateProductRequest req) {
if (req.name == null || req.name.isBlank()) throw new IllegalArgumentException("name必填");
if (req.unitId == null) throw new IllegalArgumentException("unitId必填");
if (req.safeMin != null && req.safeMax != null) {
if (req.safeMin.compareTo(req.safeMax) > 0) throw new IllegalArgumentException("安全库存区间不合法");
}
if (req.barcode != null && !req.barcode.isBlank()) {
if (productRepository.existsByShopIdAndBarcode(shopId, req.barcode)) {
// 更新时允许自己相同由Controller层在调用前判定并跳过简化此处逻辑
}
}
}
private void upsertPrice(Long userId, LocalDateTime now, Long productId, Long shopId, ProductDtos.Prices prices) {
if (prices == null) prices = new ProductDtos.Prices();
java.util.Optional<ProductPrice> existed = priceRepository.findById(productId);
ProductPrice pr = existed.orElseGet(ProductPrice::new);
pr.setProductId(productId);
pr.setShopId(shopId);
pr.setUserId(userId);
pr.setPurchasePrice(nvl(prices.purchasePrice, BigDecimal.ZERO));
pr.setRetailPrice(nvl(prices.retailPrice, BigDecimal.ZERO));
// 前端不再传分销价:仅当入参提供时更新;新建记录若未提供则置 0
if (prices.distributionPrice != null) {
pr.setDistributionPrice(prices.distributionPrice);
} else if (existed.isEmpty()) {
pr.setDistributionPrice(BigDecimal.ZERO);
}
pr.setWholesalePrice(nvl(prices.wholesalePrice, BigDecimal.ZERO));
pr.setBigClientPrice(nvl(prices.bigClientPrice, BigDecimal.ZERO));
pr.setUpdatedAt(now);
priceRepository.save(pr);
}
private void upsertInventory(Long userId, LocalDateTime now, Long productId, Long shopId, BigDecimal stock) {
Inventory inv = inventoryRepository.findById(productId).orElseGet(Inventory::new);
inv.setProductId(productId);
inv.setShopId(shopId);
inv.setUserId(userId);
inv.setQuantity(nvl(stock, BigDecimal.ZERO));
inv.setUpdatedAt(now);
inventoryRepository.save(inv);
}
private void syncImages(Long userId, Long productId, Long shopId, java.util.List<String> images) {
imageRepository.deleteByProductId(productId);
if (images == null) return;
int idx = 0;
for (String url : images) {
if (url == null || url.isBlank()) continue;
ProductImage img = new ProductImage();
img.setShopId(shopId);
img.setUserId(userId);
img.setProductId(productId);
img.setUrl(url);
img.setSortOrder(idx++);
imageRepository.save(img);
}
}
private static <T> T nvl(T v, T def) { return v != null ? v : def; }
private static String emptyToNull(String s) { return (s == null || s.isBlank()) ? null : s; }
}

View File

@@ -23,3 +23,13 @@ spring.datasource.hikari.idle-timeout=300000
spring.datasource.hikari.max-lifetime=600000
spring.datasource.hikari.keepalive-time=300000
spring.datasource.hikari.connection-timeout=30000
# 附件占位图配置(使用环境变量注入路径)
# WINDOWS 示例: setx ATTACHMENTS_PLACEHOLDER_IMAGE "C:\\Users\\21826\\Desktop\\Wj\\PartsInquiry\\backend\\picture\\屏幕截图 2025-08-14 134657.png"
# LINUX/Mac 示例: export ATTACHMENTS_PLACEHOLDER_IMAGE=/path/to/placeholder.png
attachments.placeholder.image-path=${ATTACHMENTS_PLACEHOLDER_IMAGE}
attachments.placeholder.url-path=${ATTACHMENTS_PLACEHOLDER_URL:/api/attachments/placeholder}
# 应用默认上下文(用于开发/演示环境)
app.defaults.shop-id=${APP_DEFAULT_SHOP_ID:1}
app.defaults.user-id=${APP_DEFAULT_USER_ID:2}

View File

@@ -7,6 +7,30 @@ info:
servers:
- url: /
paths:
/api/dashboard/overview:
get:
summary: 首页概览(✅ Fully Implemented
description: 订单口径的今日销售额approved、近似本月毛利按当前进价近似与库存总量。支持 X-Shop-Id 或 X-User-Id优先从用户解析店铺。后端与前端均已接入。
parameters:
- in: header
name: X-Shop-Id
required: false
schema:
type: integer
description: 店铺ID缺省为 1
- in: header
name: X-User-Id
required: false
schema:
type: integer
description: 用户ID当未提供 X-Shop-Id 时将用其所属店铺进行统计
responses:
'200':
description: 成功
content:
application/json:
schema:
$ref: '#/components/schemas/DashboardOverview'
/api/notices:
get:
summary: 公告列表(✅ Fully Implemented
@@ -104,8 +128,8 @@ paths:
format: int64
/api/products:
get:
summary: 商品搜索(❌ Partially Implemented
description: 前端已接入查询参数 kw/page/size,后端待实现或对齐
summary: 商品搜索(✅ Fully Implemented
description: 支持 kw/page/size/categoryId返回 {list:[]} 以兼容前端
parameters:
- in: query
name: kw
@@ -131,6 +155,178 @@ paths:
- type: array
items:
$ref: '#/components/schemas/Product'
post:
summary: 新建商品(✅ Fully Implemented
description: 保存商品、价格、库存与图片(当前图片统一占位图 URL
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateProductRequest'
responses:
'200':
description: 成功
content:
application/json:
schema:
type: object
properties:
id:
type: integer
format: int64
/api/products/{id}:
get:
summary: 商品详情(✅ Fully Implemented
parameters:
- in: path
name: id
required: true
schema: { type: integer }
responses:
'200':
description: 成功
content:
application/json:
schema:
$ref: '#/components/schemas/ProductDetail'
put:
summary: 更新商品(✅ Fully Implemented
parameters:
- in: path
name: id
required: true
schema: { type: integer }
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateProductRequest'
responses:
'200':
description: 成功
/api/product-categories:
get:
summary: 类别列表(✅ Fully Implemented
responses:
'200':
description: 成功
content:
application/json:
schema:
oneOf:
- type: array
items: { $ref: '#/components/schemas/Category' }
- type: object
properties:
list:
type: array
items: { $ref: '#/components/schemas/Category' }
post:
summary: 新增类别(❌ Partially Implemented
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
name: { type: string }
/api/product-categories/{id}:
put:
summary: 更新类别(❌ Partially Implemented
parameters:
- in: path
name: id
required: true
schema: { type: integer }
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
name: { type: string }
delete:
summary: 删除类别(❌ Partially Implemented
parameters:
- in: path
name: id
required: true
schema: { type: integer }
responses: { '200': { description: 成功 } }
/api/product-units:
get:
summary: 单位列表(✅ Fully Implemented
responses:
'200':
description: 成功
content:
application/json:
schema:
oneOf:
- type: array
items: { $ref: '#/components/schemas/Unit' }
- type: object
properties:
list:
type: array
items: { $ref: '#/components/schemas/Unit' }
post:
summary: 新增单位(❌ Partially Implemented
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
name: { type: string }
/api/product-units/{id}:
put:
summary: 更新单位(❌ Partially Implemented
parameters:
- in: path
name: id
required: true
schema: { type: integer }
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
name: { type: string }
delete:
summary: 删除单位(❌ Partially Implemented
parameters:
- in: path
name: id
required: true
schema: { type: integer }
responses: { '200': { description: 成功 } }
/api/product-settings:
get:
summary: 货品设置读取(❌ Partially Implemented
responses:
'200':
description: 成功
content:
application/json:
schema:
$ref: '#/components/schemas/ProductSettings'
put:
summary: 货品设置保存(❌ Partially Implemented
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/ProductSettings'
responses: { '200': { description: 成功 } }
- type: object
properties:
list:
@@ -195,8 +391,63 @@ paths:
format: int64
orderNo:
type: string
/api/attachments:
post:
summary: 上传附件(✅ Fully Implemented占位图方案
description: 接收 multipart 上传但忽略文件内容,始终返回占位图 URL后端配置项 `attachments.placeholder.image-path` 指向本地占位图片URL 固定 `/api/attachments/placeholder` 可通过 `attachments.placeholder.url-path` 覆盖)。
requestBody:
required: true
content:
multipart/form-data:
schema:
type: object
properties:
file:
type: string
format: binary
ownerType:
type: string
ownerId:
type: string
responses:
'200':
description: 成功
content:
application/json:
schema:
type: object
properties:
url:
type: string
/api/attachments/placeholder:
get:
summary: 附件占位图读取(✅ Fully Implemented
description: 返回后端配置的本地占位图内容,路径由 `attachments.placeholder.image-path` 指定。
responses:
'200':
description: 图片二进制
content:
image/png:
schema:
type: string
format: binary
components:
schemas:
DashboardOverview:
type: object
properties:
todaySalesAmount:
type: number
example: 1250.00
monthSalesAmount:
type: number
example: 26500.00
monthGrossProfit:
type: number
example: 3560.25
stockTotalQuantity:
type: number
example: 1300
Notice:
type: object
properties:
@@ -306,6 +557,67 @@ components:
type: number
stock:
type: number
ProductDetail:
allOf:
- $ref: '#/components/schemas/Product'
- type: object
properties:
brand: { type: string }
model: { type: string }
spec: { type: string }
categoryId: { type: integer, format: int64, nullable: true }
unitId: { type: integer, format: int64 }
safeMin: { type: number, nullable: true }
safeMax: { type: number, nullable: true }
purchasePrice: { type: number }
retailPrice: { type: number }
distributionPrice: { type: number }
wholesalePrice: { type: number }
bigClientPrice: { type: number }
images:
type: array
items:
type: object
properties: { url: { type: string } }
CreateProductRequest:
type: object
properties:
name: { type: string }
barcode: { type: string, nullable: true }
brand: { type: string, nullable: true }
model: { type: string, nullable: true }
spec: { type: string, nullable: true }
categoryId: { type: integer, format: int64, nullable: true }
unitId: { type: integer, format: int64 }
safeMin: { type: number, nullable: true }
safeMax: { type: number, nullable: true }
prices:
type: object
properties:
purchasePrice: { type: number }
retailPrice: { type: number }
distributionPrice: { type: number }
wholesalePrice: { type: number }
bigClientPrice: { type: number }
stock: { type: number, nullable: true }
images:
type: array
items: { type: string, description: '图片URL' }
Category:
type: object
properties:
id: { type: integer, format: int64 }
name: { type: string }
Unit:
type: object
properties:
id: { type: integer, format: int64 }
name: { type: string }
ProductSettings:
type: object
properties:
hideZeroStock: { type: boolean }
hidePurchasePrice: { type: boolean }
Customer:
type: object
properties:

View File

@@ -3,10 +3,14 @@
const envBaseUrl = (typeof process !== 'undefined' && process.env && (process.env.VITE_APP_API_BASE_URL || process.env.API_BASE_URL)) || '';
const storageBaseUrl = typeof uni !== 'undefined' ? (uni.getStorageSync('API_BASE_URL') || '') : '';
const fallbackBaseUrl = 'http://localhost:8080';
const fallbackBaseUrl = 'http://192.168.31.193:8080';
export const API_BASE_URL = (envBaseUrl || storageBaseUrl || fallbackBaseUrl).replace(/\/$/, '');
// 多地址候选(按优先级顺序,自动去重与去尾斜杠)
const candidateBases = [envBaseUrl, storageBaseUrl, fallbackBaseUrl, 'http://127.0.0.1:8080', 'http://localhost:8080'];
export const API_BASE_URL_CANDIDATES = Array.from(new Set(candidateBases.filter(Boolean))).map(u => String(u).replace(/\/$/, ''));
const envShopId = (typeof process !== 'undefined' && process.env && (process.env.VITE_APP_SHOP_ID || process.env.SHOP_ID)) || '';
const storageShopId = typeof uni !== 'undefined' ? (uni.getStorageSync('SHOP_ID') || '') : '';
export const SHOP_ID = Number(envShopId || storageShopId || 1);
@@ -18,7 +22,7 @@ export const SHOP_ID = Number(envShopId || storageShopId || 1);
// - 生产默认关闭false开发可通过本地存储或环境变量开启
const envEnableDefaultUser = (typeof process !== 'undefined' && process.env && (process.env.VITE_APP_ENABLE_DEFAULT_USER || process.env.ENABLE_DEFAULT_USER)) || '';
const storageEnableDefaultUser = typeof uni !== 'undefined' ? (uni.getStorageSync('ENABLE_DEFAULT_USER') || '') : '';
export const ENABLE_DEFAULT_USER = String(envEnableDefaultUser || storageEnableDefaultUser || 'false').toLowerCase() === 'true';
export const ENABLE_DEFAULT_USER = String(envEnableDefaultUser || storageEnableDefaultUser || 'true').toLowerCase() === 'true';
const envDefaultUserId = (typeof process !== 'undefined' && process.env && (process.env.VITE_APP_DEFAULT_USER_ID || process.env.DEFAULT_USER_ID)) || '';
const storageDefaultUserId = typeof uni !== 'undefined' ? (uni.getStorageSync('DEFAULT_USER_ID') || '') : '';

View File

@@ -16,3 +16,4 @@ export const EXPENSE_CATEGORIES = [
]

View File

@@ -1,4 +1,4 @@
import { API_BASE_URL, SHOP_ID, ENABLE_DEFAULT_USER, DEFAULT_USER_ID } from './config.js'
import { API_BASE_URL, API_BASE_URL_CANDIDATES, SHOP_ID, ENABLE_DEFAULT_USER, DEFAULT_USER_ID } from './config.js'
function buildUrl(path) {
if (!path) return API_BASE_URL
@@ -6,22 +6,26 @@ function buildUrl(path) {
return API_BASE_URL + (path.startsWith('/') ? path : '/' + path)
}
function requestWithFallback(options, candidates, idx, resolve, reject) {
const base = candidates[idx] || API_BASE_URL
const url = options.url.replace(/^https?:\/\/[^/]+/, base)
uni.request({ ...options, url, success: (res) => {
const { statusCode, data } = res
if (statusCode >= 200 && statusCode < 300) return resolve(data)
if (idx + 1 < candidates.length) return requestWithFallback(options, candidates, idx + 1, resolve, reject)
reject(new Error('HTTP ' + statusCode))
}, fail: (err) => {
if (idx + 1 < candidates.length) return requestWithFallback(options, candidates, idx + 1, resolve, reject)
reject(err)
} })
}
export function get(path, params = {}) {
return new Promise((resolve, reject) => {
const headers = { 'X-Shop-Id': SHOP_ID }
if (ENABLE_DEFAULT_USER && DEFAULT_USER_ID) headers['X-User-Id'] = DEFAULT_USER_ID
uni.request({
url: buildUrl(path),
method: 'GET',
data: params,
header: headers,
success: (res) => {
const { statusCode, data } = res
if (statusCode >= 200 && statusCode < 300) return resolve(data)
reject(new Error('HTTP ' + statusCode))
},
fail: (err) => reject(err)
})
const options = { url: buildUrl(path), method: 'GET', data: params, header: headers }
requestWithFallback(options, API_BASE_URL_CANDIDATES, 0, resolve, reject)
})
}
@@ -30,19 +34,63 @@ export function post(path, body = {}) {
return new Promise((resolve, reject) => {
const headers = { 'Content-Type': 'application/json', 'X-Shop-Id': SHOP_ID }
if (ENABLE_DEFAULT_USER && DEFAULT_USER_ID) headers['X-User-Id'] = DEFAULT_USER_ID
uni.request({
url: buildUrl(path),
method: 'POST',
data: body,
header: headers,
success: (res) => {
const { statusCode, data } = res
if (statusCode >= 200 && statusCode < 300) return resolve(data)
reject(new Error('HTTP ' + statusCode))
},
fail: (err) => reject(err)
})
const options = { url: buildUrl(path), method: 'POST', data: body, header: headers }
requestWithFallback(options, API_BASE_URL_CANDIDATES, 0, resolve, reject)
})
}
export function put(path, body = {}) {
return new Promise((resolve, reject) => {
const headers = { 'Content-Type': 'application/json', 'X-Shop-Id': SHOP_ID }
if (ENABLE_DEFAULT_USER && DEFAULT_USER_ID) headers['X-User-Id'] = DEFAULT_USER_ID
const options = { url: buildUrl(path), method: 'PUT', data: body, header: headers }
requestWithFallback(options, API_BASE_URL_CANDIDATES, 0, resolve, reject)
})
}
export function del(path, body = {}) {
return new Promise((resolve, reject) => {
const headers = { 'Content-Type': 'application/json', 'X-Shop-Id': SHOP_ID }
if (ENABLE_DEFAULT_USER && DEFAULT_USER_ID) headers['X-User-Id'] = DEFAULT_USER_ID
const options = { url: buildUrl(path), method: 'DELETE', data: body, header: headers }
requestWithFallback(options, API_BASE_URL_CANDIDATES, 0, resolve, reject)
})
}
function uploadWithFallback(options, candidates, idx, resolve, reject) {
const base = candidates[idx] || API_BASE_URL
const url = options.url.replace(/^https?:\/\/[^/]+/, base)
const uploadOptions = { ...options, url }
uni.uploadFile({
...uploadOptions,
success: (res) => {
const statusCode = res.statusCode || 0
if (statusCode >= 200 && statusCode < 300) {
try {
const data = typeof res.data === 'string' ? JSON.parse(res.data) : res.data
return resolve(data)
} catch (e) {
return resolve(res.data)
}
}
if (idx + 1 < candidates.length) return uploadWithFallback(options, candidates, idx + 1, resolve, reject)
reject(new Error('HTTP ' + statusCode))
},
fail: (err) => {
if (idx + 1 < candidates.length) return uploadWithFallback(options, candidates, idx + 1, resolve, reject)
reject(err)
}
})
}
// 文件上传封装:自动注入租户/用户头并进行多地址回退
export function upload(path, filePath, formData = {}, name = 'file') {
return new Promise((resolve, reject) => {
const header = { 'X-Shop-Id': SHOP_ID }
if (ENABLE_DEFAULT_USER && DEFAULT_USER_ID) header['X-User-Id'] = DEFAULT_USER_ID
const options = { url: buildUrl(path), filePath, name, formData, header }
uploadWithFallback(options, API_BASE_URL_CANDIDATES, 0, resolve, reject)
})
}

View File

@@ -0,0 +1,164 @@
<template>
<view class="uploader">
<view class="grid" :style="{height: areaHeight+'rpx'}">
<movable-area class="area" :style="{height: areaHeight+'rpx'}">
<movable-view
v-for="(img, index) in innerList"
:key="img.uid"
class="cell"
:style="cellStyle(index)"
:direction="'all'"
:damping="40"
:friction="2"
:x="img.x"
:y="img.y"
@change="onMoving(index, $event)"
@touchend="onMoveEnd(index)"
>
<image :src="img.url" mode="aspectFill" class="thumb" @click="preview(index)" />
<view class="remove" @click.stop="remove(index)">×</view>
</movable-view>
<view v-if="innerList.length < max" class="adder" @click="choose">
<text></text>
</view>
</movable-area>
</view>
</view>
</template>
<script>
import { upload } from '../common/http.js'
const ITEM_SIZE = 210 // rpx
const GAP = 18 // rpx
const COLS = 3
function px(rpx) {
// 以 750 设计稿计算;此函数仅用于内部位置计算,不写入样式
return rpx
}
export default {
name: 'ImageUploader',
props: {
modelValue: { type: Array, default: () => [] },
max: { type: Number, default: 9 },
uploadPath: { type: String, default: '/api/attachments' },
uploadFieldName: { type: String, default: 'file' },
formData: { type: Object, default: () => ({ ownerType: 'product' }) }
},
data() {
return {
innerList: []
}
},
computed: {
areaHeight() {
const rows = Math.ceil((this.innerList.length + 1) / COLS) || 1
return rows * ITEM_SIZE + (rows - 1) * GAP
}
},
watch: {
modelValue: {
immediate: true,
handler(list) {
const mapped = (list || []).map((u, i) => ({
uid: String(i) + '_' + (u.id || u.url || Math.random().toString(36).slice(2)),
url: typeof u === 'string' ? u : (u.url || ''),
x: this.posOf(i).x,
y: this.posOf(i).y
}))
this.innerList = mapped
}
}
},
methods: {
posOf(index) {
const row = Math.floor(index / COLS)
const col = index % COLS
return { x: px(col * (ITEM_SIZE + GAP)), y: px(row * (ITEM_SIZE + GAP)) }
},
cellStyle(index) {
return {
width: ITEM_SIZE + 'rpx',
height: ITEM_SIZE + 'rpx'
}
},
preview(index) {
uni.previewImage({ urls: this.innerList.map(i => i.url), current: index })
},
remove(index) {
this.innerList.splice(index, 1)
this.reflow()
this.emit()
},
choose() {
const remain = this.max - this.innerList.length
if (remain <= 0) return
uni.chooseImage({ count: remain, success: async (res) => {
for (const path of res.tempFilePaths) {
await this.doUpload(path)
}
}})
},
async doUpload(filePath) {
try {
const resp = await upload(this.uploadPath, filePath, this.formData, this.uploadFieldName)
const url = resp?.url || resp?.data?.url || resp?.path || ''
if (!url) throw new Error('上传响应无 url')
this.innerList.push({ uid: Math.random().toString(36).slice(2), url, ...this.posOf(this.innerList.length) })
this.reflow()
this.emit()
} catch (e) {
uni.showToast({ title: '上传失败', icon: 'none' })
}
},
onMoving(index, e) {
// 实时更新移动中元素的位置
const { x, y } = e.detail
this.innerList[index].x = x
this.innerList[index].y = y
},
onMoveEnd(index) {
// 根据落点推算新的索引
const mv = this.innerList[index]
const col = Math.round(mv.x / (ITEM_SIZE + GAP))
const row = Math.round(mv.y / (ITEM_SIZE + GAP))
let newIndex = row * COLS + col
newIndex = Math.max(0, Math.min(newIndex, this.innerList.length - 1))
if (newIndex !== index) {
const moved = this.innerList.splice(index, 1)[0]
this.innerList.splice(newIndex, 0, moved)
}
this.reflow()
this.emit()
},
reflow() {
this.innerList.forEach((it, i) => {
const p = this.posOf(i)
it.x = p.x
it.y = p.y
})
},
emit() {
this.$emit('update:modelValue', this.innerList.map(i => i.url))
this.$emit('change', this.innerList.map(i => i.url))
}
}
}
</script>
<style>
.uploader { padding: 12rpx; background: #fff; }
.grid { position: relative; }
.area { width: 100%; position: relative; }
.cell { position: absolute; border-radius: 12rpx; overflow: hidden; box-shadow: 0 0 1rpx rgba(0,0,0,0.08); }
.thumb { width: 100%; height: 100%; }
.remove { position: absolute; right: 6rpx; top: 6rpx; background: rgba(0,0,0,0.45); color: #fff; width: 40rpx; height: 40rpx; text-align: center; line-height: 40rpx; border-radius: 20rpx; font-size: 28rpx; }
.adder { width: 210rpx; height: 210rpx; border: 2rpx dashed #ccc; border-radius: 12rpx; display: flex; align-items: center; justify-content: center; color: #999; position: absolute; left: 0; top: 0; }
</style>

View File

@@ -18,6 +18,36 @@
"navigationBarTitleText": "选择商品"
}
},
{
"path": "pages/product/list",
"style": {
"navigationBarTitleText": "货品列表"
}
},
{
"path": "pages/product/form",
"style": {
"navigationBarTitleText": "编辑货品"
}
},
{
"path": "pages/product/categories",
"style": {
"navigationBarTitleText": "类别管理"
}
},
{
"path": "pages/product/units",
"style": {
"navigationBarTitleText": "单位管理"
}
},
{
"path": "pages/product/settings",
"style": {
"navigationBarTitleText": "货品设置"
}
},
{
"path": "pages/customer/select",
"style": {

View File

@@ -43,3 +43,4 @@
</style>

View File

@@ -48,3 +48,4 @@
</style>

View File

@@ -69,7 +69,7 @@
<view class="tab" :class="{ active: activeTab==='home' }" @click="activeTab='home'">
<text>首页</text>
</view>
<view class="tab" :class="{ active: activeTab==='product' }" @click="activeTab='product'">
<view class="tab" :class="{ active: activeTab==='product' }" @click="goProduct">
<text>货品</text>
</view>
<view class="tab primary" @click="onCreateOrder">
@@ -99,6 +99,7 @@
loadingNotices: false,
noticeError: '',
features: [
{ key: 'product', title: '货品', img: '/static/icons/product.png', emoji: '📦' },
{ key: 'customer', title: '客户', img: '/static/icons/customer.png', emoji: '👥' },
{ key: 'sale', title: '销售', img: '/static/icons/sale.png', emoji: '💰' },
{ key: 'account', title: '账户', img: '/static/icons/account.png', emoji: '💳' },
@@ -118,12 +119,14 @@
methods: {
async fetchMetrics() {
try {
const d = await get('/api/metrics/overview')
const d = await get('/api/dashboard/overview')
const toNum = v => (typeof v === 'number' ? v : Number(v || 0))
this.kpi = {
todaySales: (d && d.todaySales) || '0.00',
monthSales: (d && d.monthSales) || '0.00',
monthProfit: (d && d.monthProfit) || '0.00',
stockCount: (d && d.stockCount) || '0'
...this.kpi,
todaySales: toNum(d && d.todaySalesAmount).toFixed(2),
monthSales: toNum(d && d.monthSalesAmount).toFixed(2),
monthProfit: toNum(d && d.monthGrossProfit).toFixed(2),
stockCount: String((d && d.stockTotalQuantity) != null ? d.stockTotalQuantity : 0)
}
} catch (e) {
// 忽略错误,保留默认值
@@ -145,8 +148,16 @@
}
},
onFeatureTap(item) {
if (item.key === 'product') {
uni.navigateTo({ url: '/pages/product/list' })
return
}
uni.showToast({ title: item.title + '(开发中)', icon: 'none' })
},
goProduct() {
this.activeTab = 'product'
uni.navigateTo({ url: '/pages/product/list' })
},
onCreateOrder() {
uni.navigateTo({ url: '/pages/order/create' })
},

View File

@@ -0,0 +1,67 @@
<template>
<view class="page">
<view class="toolbar">
<input v-model.trim="name" placeholder="新类别名称" />
<button size="mini" @click="create">新增</button>
</view>
<scroll-view scroll-y class="list">
<view class="item" v-for="c in list" :key="c.id">
<input v-model.trim="c.name" />
<view class="ops">
<button size="mini" @click="update(c)">保存</button>
<button size="mini" type="warn" @click="remove(c)">删除</button>
</view>
</view>
</scroll-view>
</view>
</template>
<script>
import { get, post, put, del } from '../../common/http.js'
export default {
data() {
return { name: '', list: [] }
},
onLoad() { this.reload() },
methods: {
async reload() {
try {
const res = await get('/api/product-categories')
this.list = Array.isArray(res?.list) ? res.list : (Array.isArray(res) ? res : [])
} catch (_) {}
},
async create() {
if (!this.name) return
await post('/api/product-categories', { name: this.name })
this.name = ''
this.reload()
},
async update(c) {
await put('/api/product-categories/' + c.id, { name: c.name })
uni.showToast({ title: '已保存', icon: 'success' })
},
async remove(c) {
uni.showModal({ content: '确定删除该类别?', success: async (r) => {
if (!r.confirm) return
await del('/api/product-categories/' + c.id)
this.reload()
}})
}
}
}
</script>
<style>
.page { display:flex; flex-direction: column; height: 100vh; }
.toolbar { display:flex; gap: 12rpx; padding: 16rpx; background:#fff; }
.toolbar input { flex:1; background:#f6f6f6; border-radius: 12rpx; padding: 12rpx; }
.list { flex:1; }
.item { display:flex; gap: 12rpx; align-items:center; padding: 16rpx; background:#fff; border-bottom: 1rpx solid #f1f1f1; }
.item input { flex:1; background:#f7f7f7; border-radius: 10rpx; padding: 12rpx; }
.ops { display:flex; gap: 10rpx; }
</style>

View File

@@ -0,0 +1,213 @@
<template>
<scroll-view scroll-y class="page">
<view class="card">
<view class="row">
<text class="label">商品名称</text>
<input v-model.trim="form.name" placeholder="必填" />
</view>
<view class="row">
<text class="label">条形码</text>
<input v-model.trim="form.barcode" placeholder="可扫码或输入" />
<!-- #ifdef APP-PLUS -->
<button size="mini" @click="scan">扫码</button>
<!-- #endif -->
</view>
<view class="row">
<text class="label">品牌/型号/规格/产地</text>
</view>
<view class="row">
<input v-model.trim="form.brand" placeholder="品牌" />
</view>
<view class="row">
<input v-model.trim="form.model" placeholder="型号" />
</view>
<view class="row">
<input v-model.trim="form.spec" placeholder="规格" />
</view>
<view class="row">
<input v-model.trim="form.origin" placeholder="产地" />
</view>
<view class="row">
<picker mode="selector" :range="unitNames" @change="onPickUnit">
<view class="picker">主单位{{ unitLabel }}</view>
</picker>
<picker mode="selector" :range="categoryNames" @change="onPickCategory">
<view class="picker">类别{{ categoryLabel }}</view>
</picker>
</view>
</view>
<view class="card">
<view class="row">
<text class="label">库存与安全库存</text>
</view>
<view class="row">
<input type="number" v-model.number="form.stock" placeholder="当前库存" />
<input type="number" v-model.number="form.safeMin" placeholder="安全库存下限" />
<input type="number" v-model.number="form.safeMax" placeholder="安全库存上限" />
</view>
</view>
<view class="card">
<view class="row">
<text class="label">价格进价/零售/批发/大单</text>
</view>
<view class="row prices">
<input type="number" v-model.number="form.purchasePrice" placeholder="进货价" />
<input type="number" v-model.number="form.retailPrice" placeholder="零售价" />
<input type="number" v-model.number="form.wholesalePrice" placeholder="批发价" />
<input type="number" v-model.number="form.bigClientPrice" placeholder="大单价" />
</view>
</view>
<view class="card">
<text class="label">图片</text>
<ImageUploader v-model="form.images" :formData="{ ownerType: 'product' }" />
</view>
<view class="card">
<text class="label">备注</text>
<textarea v-model.trim="form.remark" placeholder="可选" auto-height />
</view>
<view class="fixed">
<button type="default" @click="save(false)">保存</button>
<button type="primary" @click="save(true)">保存并继续</button>
</view>
</scroll-view>
</template>
<script>
import ImageUploader from '../../components/ImageUploader.vue'
import { get, post, put } from '../../common/http.js'
export default {
components: { ImageUploader },
data() {
return {
id: '',
form: {
name: '', barcode: '', brand: '', model: '', spec: '', origin: '',
categoryId: '', unitId: '',
stock: null, safeMin: null, safeMax: null,
purchasePrice: null, retailPrice: null, wholesalePrice: null, bigClientPrice: null,
images: [], remark: ''
},
units: [],
categories: []
}
},
onLoad(query) {
this.id = query?.id || ''
this.bootstrap()
},
computed: {
unitNames() { return this.units.map(u => u.name) },
categoryNames() { return this.categories.map(c => c.name) },
unitLabel() {
const u = this.units.find(x => String(x.id) === String(this.form.unitId))
return u ? u.name : '选择单位'
},
categoryLabel() {
const c = this.categories.find(x => String(x.id) === String(this.form.categoryId))
return c ? c.name : '选择类别'
}
},
methods: {
async bootstrap() {
await Promise.all([this.fetchUnits(), this.fetchCategories()])
if (this.id) this.loadDetail()
},
async fetchUnits() {
try {
const res = await get('/api/product-units')
this.units = Array.isArray(res?.list) ? res.list : (Array.isArray(res) ? res : [])
} catch (_) {}
},
async fetchCategories() {
try {
const res = await get('/api/product-categories')
this.categories = Array.isArray(res?.list) ? res.list : (Array.isArray(res) ? res : [])
} catch (_) {}
},
onPickUnit(e) {
const idx = Number(e.detail.value); const u = this.units[idx]
this.form.unitId = u ? u.id : ''
},
onPickCategory(e) {
const idx = Number(e.detail.value); const c = this.categories[idx]
this.form.categoryId = c ? c.id : ''
},
scan() {
uni.scanCode({ onlyFromCamera: false, success: (res) => {
this.form.barcode = res.result
}})
},
async loadDetail() {
try {
const data = await get('/api/products/' + this.id)
Object.assign(this.form, {
name: data.name,
barcode: data.barcode, brand: data.brand, model: data.model, spec: data.spec, origin: data.origin,
categoryId: data.categoryId, unitId: data.unitId,
stock: data.stock,
safeMin: data.safeMin, safeMax: data.safeMax,
purchasePrice: data.purchasePrice, retailPrice: data.retailPrice,
wholesalePrice: data.wholesalePrice, bigClientPrice: data.bigClientPrice,
images: (data.images || []).map(i => i.url || i)
})
} catch (_) {}
},
validate() {
if (!this.form.name) { uni.showToast({ title: '请填写名称', icon: 'none' }); return false }
if (this.form.safeMin != null && this.form.safeMax != null && Number(this.form.safeMin) > Number(this.form.safeMax)) {
uni.showToast({ title: '安全库存区间不合法', icon: 'none' }); return false
}
return true
},
buildPayload() {
const f = this.form
return {
name: f.name, barcode: f.barcode, brand: f.brand, model: f.model, spec: f.spec, origin: f.origin,
categoryId: f.categoryId || null, unitId: f.unitId,
safeMin: f.safeMin, safeMax: f.safeMax,
prices: {
purchasePrice: f.purchasePrice, retailPrice: f.retailPrice, wholesalePrice: f.wholesalePrice, bigClientPrice: f.bigClientPrice
},
stock: f.stock,
images: f.images,
remark: f.remark
}
},
async save(goOn) {
if (!this.validate()) return
const payload = this.buildPayload()
try {
if (this.id) await put('/api/products/' + this.id, payload)
else await post('/api/products', payload)
uni.showToast({ title: '保存成功', icon: 'success' })
if (goOn && !this.id) {
this.form = { name: '', barcode: '', brand: '', model: '', spec: '', origin: '', categoryId: '', unitId: '', stock: null, safeMin: null, safeMax: null, purchasePrice: null, retailPrice: null, wholesalePrice: null, bigClientPrice: null, images: [], remark: '' }
} else {
setTimeout(() => uni.navigateBack(), 400)
}
} catch (e) {
uni.showToast({ title: '保存失败', icon: 'none' })
}
}
}
}
</script>
<style>
.page { background:#f6f6f6; height: 100vh; }
.card { background:#fff; margin: 16rpx; padding: 16rpx; border-radius: 12rpx; }
.row { display:flex; gap: 12rpx; align-items: center; margin-bottom: 12rpx; }
.label { width: 180rpx; color:#666; }
.row input { flex:1; background:#f7f7f7; border-radius: 10rpx; padding: 12rpx; }
.picker { padding: 8rpx 12rpx; background:#f0f0f0; border-radius: 10rpx; color:#666; margin-left: 8rpx; }
.prices input { width: 30%; }
.fixed { position: fixed; left: 0; right: 0; bottom: 0; background:#fff; padding: 12rpx 16rpx; display:flex; gap: 16rpx; }
</style>

View File

@@ -0,0 +1,133 @@
<template>
<view class="page">
<view class="tabs">
<view class="tab" :class="{active: tab==='all'}" @click="switchTab('all')">全部</view>
<view class="tab" :class="{active: tab==='category'}" @click="switchTab('category')">按类别</view>
</view>
<view class="search">
<input v-model.trim="query.kw" placeholder="输入名称/条码/规格查询" @confirm="reload" />
<picker mode="selector" :range="categoryNames" v-if="tab==='category'" @change="onPickCategory">
<view class="picker">{{ categoryLabel }}</view>
</picker>
<button size="mini" @click="reload">查询</button>
</view>
<scroll-view scroll-y class="list" @scrolltolower="loadMore">
<block v-if="items.length">
<view class="item" v-for="it in items" :key="it.id" @click="openForm(it.id)">
<image v-if="it.cover" :src="it.cover" class="thumb" mode="aspectFill" />
<view class="content">
<view class="name">{{ it.name }}</view>
<view class="meta">{{ it.brand || '-' }} {{ it.model || '' }} {{ it.spec || '' }}</view>
<view class="meta">库存{{ it.stock ?? 0 }}
<text class="price">零售价¥{{ (it.retailPrice ?? it.price ?? 0).toFixed(2) }}</text>
</view>
</view>
</view>
</block>
<view v-else class="empty">
<text>暂无数据点击右上角新增</text>
</view>
</scroll-view>
<view class="fab" @click="openForm()"></view>
</view>
</template>
<script>
import { get } from '../../common/http.js'
export default {
data() {
return {
items: [],
query: { kw: '', page: 1, size: 20, categoryId: '' },
finished: false,
loading: false,
tab: 'all',
categories: []
}
},
onLoad() {
this.fetchCategories()
this.reload()
},
computed: {
categoryNames() { return this.categories.map(c => c.name) },
categoryLabel() {
const c = this.categories.find(x => String(x.id) === String(this.query.categoryId))
return c ? '类别:' + c.name : '选择类别'
}
},
methods: {
switchTab(t) {
this.tab = t
this.query.categoryId = ''
this.reload()
},
onPickCategory(e) {
const idx = Number(e.detail.value)
const c = this.categories[idx]
this.query.categoryId = c ? c.id : ''
this.reload()
},
async fetchCategories() {
try {
const res = await get('/api/product-categories', {})
this.categories = Array.isArray(res?.list) ? res.list : (Array.isArray(res) ? res : [])
} catch (_) {}
},
reload() {
this.items = []
this.query.page = 1
this.finished = false
this.loadMore()
},
async loadMore() {
if (this.loading || this.finished) return
this.loading = true
try {
const params = { kw: this.query.kw, page: this.query.page, size: this.query.size }
if (this.tab === 'category' && this.query.categoryId) params.categoryId = this.query.categoryId
const res = await get('/api/products', params)
const list = Array.isArray(res?.list) ? res.list : (Array.isArray(res) ? res : [])
this.items = this.items.concat(list)
if (list.length < this.query.size) this.finished = true
this.query.page += 1
} catch (e) {
uni.showToast({ title: '加载失败', icon: 'none' })
} finally {
this.loading = false
}
},
openForm(id) {
const url = '/pages/product/form' + (id ? ('?id=' + id) : '')
uni.navigateTo({ url })
}
}
}
</script>
<style>
.page { display:flex; flex-direction: column; height: 100vh; }
.tabs { display:flex; background:#fff; }
.tab { flex:1; text-align:center; padding: 20rpx 0; color:#666; }
.tab.active { color:#18b566; font-weight: 600; }
.search { display:flex; gap: 12rpx; padding: 16rpx; background:#fff; align-items: center; }
.search input { flex:1; background:#f6f6f6; border-radius: 12rpx; padding: 12rpx; }
.picker { padding: 8rpx 12rpx; background:#f0f0f0; border-radius: 10rpx; color:#666; }
.list { flex:1; }
.item { display:flex; padding: 20rpx; background:#fff; border-bottom: 1rpx solid #f1f1f1; }
.thumb { width: 120rpx; height: 120rpx; border-radius: 12rpx; margin-right: 16rpx; background:#fafafa; }
.content { flex:1; }
.name { color:#333; margin-bottom: 6rpx; font-weight: 600; }
.meta { color:#888; font-size: 24rpx; }
.price { margin-left: 20rpx; color:#f60; }
.empty { height: 60vh; display:flex; align-items:center; justify-content:center; color:#999; }
.fab { position: fixed; right: 30rpx; bottom: 120rpx; width: 100rpx; height: 100rpx; background:#18b566; color:#fff; border-radius: 50rpx; text-align:center; line-height: 100rpx; font-size: 48rpx; box-shadow: 0 8rpx 20rpx rgba(0,0,0,0.15); }
</style>

View File

@@ -47,3 +47,4 @@
</style>

View File

@@ -0,0 +1,45 @@
<template>
<view class="page">
<view class="item">
<text>隐藏零库存商品</text>
<switch :checked="settings.hideZeroStock" @change="(e)=>update('hideZeroStock', e.detail.value)" />
</view>
<view class="item">
<text>隐藏进货价</text>
<switch :checked="settings.hidePurchasePrice" @change="(e)=>update('hidePurchasePrice', e.detail.value)" />
</view>
</view>
</template>
<script>
import { get, put } from '../../common/http.js'
export default {
data() {
return { settings: { hideZeroStock: false, hidePurchasePrice: false } }
},
onLoad() { this.load() },
methods: {
async load() {
try {
const res = await get('/api/product-settings')
this.settings = { hideZeroStock: !!res?.hideZeroStock, hidePurchasePrice: !!res?.hidePurchasePrice }
} catch (_) {}
},
async update(key, val) {
const next = { ...this.settings, [key]: val }
this.settings = next
try { await put('/api/product-settings', next) } catch (_) {}
}
}
}
</script>
<style>
.page { background:#fff; }
.item { display:flex; justify-content: space-between; align-items:center; padding: 20rpx; border-bottom: 1rpx solid #f1f1f1; }
</style>

View File

@@ -0,0 +1,67 @@
<template>
<view class="page">
<view class="toolbar">
<input v-model.trim="name" placeholder="新单位名称" />
<button size="mini" @click="create">新增</button>
</view>
<scroll-view scroll-y class="list">
<view class="item" v-for="u in list" :key="u.id">
<input v-model.trim="u.name" />
<view class="ops">
<button size="mini" @click="update(u)">保存</button>
<button size="mini" type="warn" @click="remove(u)">删除</button>
</view>
</view>
</scroll-view>
</view>
</template>
<script>
import { get, post, put, del } from '../../common/http.js'
export default {
data() {
return { name: '', list: [] }
},
onLoad() { this.reload() },
methods: {
async reload() {
try {
const res = await get('/api/product-units')
this.list = Array.isArray(res?.list) ? res.list : (Array.isArray(res) ? res : [])
} catch (_) {}
},
async create() {
if (!this.name) return
await post('/api/product-units', { name: this.name })
this.name = ''
this.reload()
},
async update(u) {
await put('/api/product-units/' + u.id, { name: u.name })
uni.showToast({ title: '已保存', icon: 'success' })
},
async remove(u) {
uni.showModal({ content: '确定删除该单位?', success: async (r) => {
if (!r.confirm) return
await del('/api/product-units/' + u.id)
this.reload()
}})
}
}
}
</script>
<style>
.page { display:flex; flex-direction: column; height: 100vh; }
.toolbar { display:flex; gap: 12rpx; padding: 16rpx; background:#fff; }
.toolbar input { flex:1; background:#f6f6f6; border-radius: 12rpx; padding: 12rpx; }
.list { flex:1; }
.item { display:flex; gap: 12rpx; align-items:center; padding: 16rpx; background:#fff; border-bottom: 1rpx solid #f1f1f1; }
.item input { flex:1; background:#f7f7f7; border-radius: 10rpx; padding: 12rpx; }
.ops { display:flex; gap: 10rpx; }
</style>

View File

@@ -48,3 +48,4 @@
</style>

View File

@@ -1 +1 @@
{"version":3,"file":"app.js","sources":["App.vue","main.js"],"sourcesContent":["<script>\r\n\texport default {\r\n\t\tonLaunch: function() {\r\n\t\t\tconsole.log('App Launch')\r\n\t\t},\r\n\t\tonShow: function() {\r\n\t\t\tconsole.log('App Show')\r\n\t\t},\r\n\t\tonHide: function() {\r\n\t\t\tconsole.log('App Hide')\r\n\t\t}\r\n\t}\r\n</script>\r\n\r\n<style>\r\n\t/*每个页面公共css */\r\n</style>\r\n","import App from './App'\r\n\r\n// #ifndef VUE3\r\nimport Vue from 'vue'\r\nimport './uni.promisify.adaptor'\r\nVue.config.productionTip = false\r\nApp.mpType = 'app'\r\nconst app = new Vue({\r\n ...App\r\n})\r\napp.$mount()\r\n// #endif\r\n\r\n// #ifdef VUE3\r\nimport { createSSRApp } from 'vue'\r\nexport function createApp() {\r\n const app = createSSRApp(App)\r\n return {\r\n app\r\n }\r\n}\r\n// #endif"],"names":["uni","createSSRApp","App"],"mappings":";;;;;;;;;;;AACC,MAAK,YAAU;AAAA,EACd,UAAU,WAAW;AACpBA,kBAAAA,MAAA,MAAA,OAAA,gBAAY,YAAY;AAAA,EACxB;AAAA,EACD,QAAQ,WAAW;AAClBA,kBAAAA,MAAY,MAAA,OAAA,gBAAA,UAAU;AAAA,EACtB;AAAA,EACD,QAAQ,WAAW;AAClBA,kBAAAA,MAAY,MAAA,OAAA,iBAAA,UAAU;AAAA,EACvB;AACD;ACIM,SAAS,YAAY;AAC1B,QAAM,MAAMC,cAAY,aAACC,SAAG;AAC5B,SAAO;AAAA,IACL;AAAA,EACD;AACH;;;"}
{"version":3,"file":"app.js","sources":["App.vue","main.js"],"sourcesContent":["<script>\r\n\texport default {\r\n\t\tonLaunch: function() {\r\n\t\t\tconsole.log('App Launch')\r\n\t\t},\r\n\t\tonShow: function() {\r\n\t\t\tconsole.log('App Show')\r\n\t\t},\r\n\t\tonHide: function() {\r\n\t\t\tconsole.log('App Hide')\r\n\t\t}\r\n\t}\r\n</script>\r\n\r\n<style>\r\n\t/*每个页面公共css */\r\n</style>\r\n","import App from './App'\r\n\r\n// #ifndef VUE3\r\nimport Vue from 'vue'\r\nimport './uni.promisify.adaptor'\r\nVue.config.productionTip = false\r\nApp.mpType = 'app'\r\nconst app = new Vue({\r\n ...App\r\n})\r\napp.$mount()\r\n// #endif\r\n\r\n// #ifdef VUE3\r\nimport { createSSRApp } from 'vue'\r\nexport function createApp() {\r\n const app = createSSRApp(App)\r\n return {\r\n app\r\n }\r\n}\r\n// #endif"],"names":["uni","createSSRApp","App"],"mappings":";;;;;;;;;;;;;;;;AACC,MAAK,YAAU;AAAA,EACd,UAAU,WAAW;AACpBA,kBAAAA,MAAA,MAAA,OAAA,gBAAY,YAAY;AAAA,EACxB;AAAA,EACD,QAAQ,WAAW;AAClBA,kBAAAA,MAAY,MAAA,OAAA,gBAAA,UAAU;AAAA,EACtB;AAAA,EACD,QAAQ,WAAW;AAClBA,kBAAAA,MAAY,MAAA,OAAA,iBAAA,UAAU;AAAA,EACvB;AACD;ACIM,SAAS,YAAY;AAC1B,QAAM,MAAMC,cAAY,aAACC,SAAG;AAC5B,SAAO;AAAA,IACL;AAAA,EACD;AACH;;;"}

View File

@@ -1 +1 @@
{"version":3,"file":"config.js","sources":["common/config.js"],"sourcesContent":["// 统一配置:禁止在业务代码中硬编码\n// 优先级:环境变量(Vite/HBuilderX 构建注入) > 本地存储 > 默认值\n\nconst envBaseUrl = (typeof process !== 'undefined' && process.env && (process.env.VITE_APP_API_BASE_URL || process.env.API_BASE_URL)) || '';\nconst storageBaseUrl = typeof uni !== 'undefined' ? (uni.getStorageSync('API_BASE_URL') || '') : '';\nconst fallbackBaseUrl = 'http://localhost:8080';\n\nexport const API_BASE_URL = (envBaseUrl || storageBaseUrl || fallbackBaseUrl).replace(/\\/$/, '');\n\nconst envShopId = (typeof process !== 'undefined' && process.env && (process.env.VITE_APP_SHOP_ID || process.env.SHOP_ID)) || '';\nconst storageShopId = typeof uni !== 'undefined' ? (uni.getStorageSync('SHOP_ID') || '') : '';\nexport const SHOP_ID = Number(envShopId || storageShopId || 1);\n\n\n// 默认用户(可移除):\n// - 用途:开发/演示环境自动将用户固定为“张老板”id=2\n// - 开关优先级:环境变量 > 本地存储 > 默认值\n// - 生产默认关闭false开发可通过本地存储或环境变量开启\nconst envEnableDefaultUser = (typeof process !== 'undefined' && process.env && (process.env.VITE_APP_ENABLE_DEFAULT_USER || process.env.ENABLE_DEFAULT_USER)) || '';\nconst storageEnableDefaultUser = typeof uni !== 'undefined' ? (uni.getStorageSync('ENABLE_DEFAULT_USER') || '') : '';\nexport const ENABLE_DEFAULT_USER = String(envEnableDefaultUser || storageEnableDefaultUser || 'false').toLowerCase() === 'true';\n\nconst envDefaultUserId = (typeof process !== 'undefined' && process.env && (process.env.VITE_APP_DEFAULT_USER_ID || process.env.DEFAULT_USER_ID)) || '';\nconst storageDefaultUserId = typeof uni !== 'undefined' ? (uni.getStorageSync('DEFAULT_USER_ID') || '') : '';\nexport const DEFAULT_USER_ID = Number(envDefaultUserId || storageDefaultUserId || 2);\n\n\n"],"names":["uni"],"mappings":";;AAGA,MAAM,aAAc,OAAO,YAAY,eAAe,QAAQ,QAAQ,QAAQ,IAAI,yBAAyB,QAAQ,IAAI,iBAAkB;AACzI,MAAM,iBAAiB,OAAOA,cAAG,UAAK,cAAeA,cAAAA,MAAI,eAAe,cAAc,KAAK,KAAM;AACjG,MAAM,kBAAkB;AAEZ,MAAC,gBAAgB,cAAc,kBAAkB,iBAAiB,QAAQ,OAAO,EAAE;AAE/F,MAAM,YAAa,OAAO,YAAY,eAAe,QAAQ,QAAQ,QAAQ,IAAI,oBAAoB,QAAQ,IAAI,YAAa;AAC9H,MAAM,gBAAgB,OAAOA,cAAG,UAAK,cAAeA,cAAAA,MAAI,eAAe,SAAS,KAAK,KAAM;AAC/E,MAAC,UAAU,OAAO,aAAa,iBAAiB,CAAC;AAO7D,MAAM,uBAAwB,OAAO,YAAY,eAAe,QAAQ,QAAQ,QAAQ,IAAI,gCAAgC,QAAQ,IAAI,wBAAyB;AACjK,MAAM,2BAA2B,OAAOA,cAAG,UAAK,cAAeA,cAAAA,MAAI,eAAe,qBAAqB,KAAK,KAAM;AACtG,MAAC,sBAAsB,OAAO,wBAAwB,4BAA4B,OAAO,EAAE,YAAW,MAAO;AAEzH,MAAM,mBAAoB,OAAO,YAAY,eAAe,QAAQ,QAAQ,QAAQ,IAAI,4BAA4B,QAAQ,IAAI,oBAAqB;AACrJ,MAAM,uBAAuB,OAAOA,cAAG,UAAK,cAAeA,cAAAA,MAAI,eAAe,iBAAiB,KAAK,KAAM;AAC9F,MAAC,kBAAkB,OAAO,oBAAoB,wBAAwB,CAAC;;;;;"}
{"version":3,"file":"config.js","sources":["common/config.js"],"sourcesContent":["// 统一配置:禁止在业务代码中硬编码\n// 优先级:环境变量(Vite/HBuilderX 构建注入) > 本地存储 > 默认值\n\nconst envBaseUrl = (typeof process !== 'undefined' && process.env && (process.env.VITE_APP_API_BASE_URL || process.env.API_BASE_URL)) || '';\nconst storageBaseUrl = typeof uni !== 'undefined' ? (uni.getStorageSync('API_BASE_URL') || '') : '';\nconst fallbackBaseUrl = 'http://192.168.31.193:8080';\n\nexport const API_BASE_URL = (envBaseUrl || storageBaseUrl || fallbackBaseUrl).replace(/\\/$/, '');\n\n// 多地址候选(按优先级顺序,自动去重与去尾斜杠)\nconst candidateBases = [envBaseUrl, storageBaseUrl, fallbackBaseUrl, 'http://127.0.0.1:8080', 'http://localhost:8080'];\nexport const API_BASE_URL_CANDIDATES = Array.from(new Set(candidateBases.filter(Boolean))).map(u => String(u).replace(/\\/$/, ''));\n\nconst envShopId = (typeof process !== 'undefined' && process.env && (process.env.VITE_APP_SHOP_ID || process.env.SHOP_ID)) || '';\nconst storageShopId = typeof uni !== 'undefined' ? (uni.getStorageSync('SHOP_ID') || '') : '';\nexport const SHOP_ID = Number(envShopId || storageShopId || 1);\n\n\n// 默认用户(可移除):\n// - 用途:开发/演示环境自动将用户固定为“张老板”id=2\n// - 开关优先级:环境变量 > 本地存储 > 默认值\n// - 生产默认关闭false开发可通过本地存储或环境变量开启\nconst envEnableDefaultUser = (typeof process !== 'undefined' && process.env && (process.env.VITE_APP_ENABLE_DEFAULT_USER || process.env.ENABLE_DEFAULT_USER)) || '';\nconst storageEnableDefaultUser = typeof uni !== 'undefined' ? (uni.getStorageSync('ENABLE_DEFAULT_USER') || '') : '';\nexport const ENABLE_DEFAULT_USER = String(envEnableDefaultUser || storageEnableDefaultUser || 'true').toLowerCase() === 'true';\n\nconst envDefaultUserId = (typeof process !== 'undefined' && process.env && (process.env.VITE_APP_DEFAULT_USER_ID || process.env.DEFAULT_USER_ID)) || '';\nconst storageDefaultUserId = typeof uni !== 'undefined' ? (uni.getStorageSync('DEFAULT_USER_ID') || '') : '';\nexport const DEFAULT_USER_ID = Number(envDefaultUserId || storageDefaultUserId || 2);\n\n\n"],"names":["uni"],"mappings":";;AAGA,MAAM,aAAc,OAAO,YAAY,eAAe,QAAQ,QAAQ,QAAQ,IAAI,yBAAyB,QAAQ,IAAI,iBAAkB;AACzI,MAAM,iBAAiB,OAAOA,cAAG,UAAK,cAAeA,cAAAA,MAAI,eAAe,cAAc,KAAK,KAAM;AACjG,MAAM,kBAAkB;AAEZ,MAAC,gBAAgB,cAAc,kBAAkB,iBAAiB,QAAQ,OAAO,EAAE;AAG/F,MAAM,iBAAiB,CAAC,YAAY,gBAAgB,iBAAiB,yBAAyB,uBAAuB;AACzG,MAAC,0BAA0B,MAAM,KAAK,IAAI,IAAI,eAAe,OAAO,OAAO,CAAC,CAAC,EAAE,IAAI,OAAK,OAAO,CAAC,EAAE,QAAQ,OAAO,EAAE,CAAC;AAEhI,MAAM,YAAa,OAAO,YAAY,eAAe,QAAQ,QAAQ,QAAQ,IAAI,oBAAoB,QAAQ,IAAI,YAAa;AAC9H,MAAM,gBAAgB,OAAOA,cAAG,UAAK,cAAeA,cAAAA,MAAI,eAAe,SAAS,KAAK,KAAM;AAC/E,MAAC,UAAU,OAAO,aAAa,iBAAiB,CAAC;AAO7D,MAAM,uBAAwB,OAAO,YAAY,eAAe,QAAQ,QAAQ,QAAQ,IAAI,gCAAgC,QAAQ,IAAI,wBAAyB;AACjK,MAAM,2BAA2B,OAAOA,cAAG,UAAK,cAAeA,cAAAA,MAAI,eAAe,qBAAqB,KAAK,KAAM;AACtG,MAAC,sBAAsB,OAAO,wBAAwB,4BAA4B,MAAM,EAAE,YAAW,MAAO;AAExH,MAAM,mBAAoB,OAAO,YAAY,eAAe,QAAQ,QAAQ,QAAQ,IAAI,4BAA4B,QAAQ,IAAI,oBAAqB;AACrJ,MAAM,uBAAuB,OAAOA,cAAG,UAAK,cAAeA,cAAAA,MAAI,eAAe,iBAAiB,KAAK,KAAM;AAC9F,MAAC,kBAAkB,OAAO,oBAAoB,wBAAwB,CAAC;;;;;;"}

View File

@@ -1 +1 @@
{"version":3,"file":"constants.js","sources":["common/constants.js"],"sourcesContent":["// 统一常量配置:其他收入/支出分类,禁止在业务中硬编码\r\nexport const INCOME_CATEGORIES = [\r\n\t{ key: 'sale_income', label: '销售收入' },\r\n\t{ key: 'operation_income', label: '经营所得' },\r\n\t{ key: 'interest_income', label: '利息收入' },\r\n\t{ key: 'investment_income', label: '投资收入' },\r\n\t{ key: 'other_income', label: '其它收入' }\r\n]\r\n\r\nexport const EXPENSE_CATEGORIES = [\r\n\t{ key: 'operation_expense', label: '经营支出' },\r\n\t{ key: 'office_supplies', label: '办公用品' },\r\n\t{ key: 'rent', label: '房租' },\r\n\t{ key: 'interest_expense', label: '利息支出' },\r\n\t{ key: 'other_expense', label: '其它支出' }\r\n]\r\n\r\n\r\n"],"names":[],"mappings":";AACY,MAAC,oBAAoB;AAAA,EAChC,EAAE,KAAK,eAAe,OAAO,OAAQ;AAAA,EACrC,EAAE,KAAK,oBAAoB,OAAO,OAAQ;AAAA,EAC1C,EAAE,KAAK,mBAAmB,OAAO,OAAQ;AAAA,EACzC,EAAE,KAAK,qBAAqB,OAAO,OAAQ;AAAA,EAC3C,EAAE,KAAK,gBAAgB,OAAO,OAAQ;AACvC;AAEY,MAAC,qBAAqB;AAAA,EACjC,EAAE,KAAK,qBAAqB,OAAO,OAAQ;AAAA,EAC3C,EAAE,KAAK,mBAAmB,OAAO,OAAQ;AAAA,EACzC,EAAE,KAAK,QAAQ,OAAO,KAAM;AAAA,EAC5B,EAAE,KAAK,oBAAoB,OAAO,OAAQ;AAAA,EAC1C,EAAE,KAAK,iBAAiB,OAAO,OAAQ;AACxC;;;"}
{"version":3,"file":"constants.js","sources":["common/constants.js"],"sourcesContent":["// 统一常量配置:其他收入/支出分类,禁止在业务中硬编码\r\nexport const INCOME_CATEGORIES = [\r\n\t{ key: 'sale_income', label: '销售收入' },\r\n\t{ key: 'operation_income', label: '经营所得' },\r\n\t{ key: 'interest_income', label: '利息收入' },\r\n\t{ key: 'investment_income', label: '投资收入' },\r\n\t{ key: 'other_income', label: '其它收入' }\r\n]\r\n\r\nexport const EXPENSE_CATEGORIES = [\r\n\t{ key: 'operation_expense', label: '经营支出' },\r\n\t{ key: 'office_supplies', label: '办公用品' },\r\n\t{ key: 'rent', label: '房租' },\r\n\t{ key: 'interest_expense', label: '利息支出' },\r\n\t{ key: 'other_expense', label: '其它支出' }\r\n]\r\n\r\n\r\n\r\n"],"names":[],"mappings":";AACY,MAAC,oBAAoB;AAAA,EAChC,EAAE,KAAK,eAAe,OAAO,OAAQ;AAAA,EACrC,EAAE,KAAK,oBAAoB,OAAO,OAAQ;AAAA,EAC1C,EAAE,KAAK,mBAAmB,OAAO,OAAQ;AAAA,EACzC,EAAE,KAAK,qBAAqB,OAAO,OAAQ;AAAA,EAC3C,EAAE,KAAK,gBAAgB,OAAO,OAAQ;AACvC;AAEY,MAAC,qBAAqB;AAAA,EACjC,EAAE,KAAK,qBAAqB,OAAO,OAAQ;AAAA,EAC3C,EAAE,KAAK,mBAAmB,OAAO,OAAQ;AAAA,EACzC,EAAE,KAAK,QAAQ,OAAO,KAAM;AAAA,EAC5B,EAAE,KAAK,oBAAoB,OAAO,OAAQ;AAAA,EAC1C,EAAE,KAAK,iBAAiB,OAAO,OAAQ;AACxC;;;"}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
{"version":3,"file":"select.js","sources":["pages/account/select.vue","../../../../Downloads/HBuilderX.4.76.2025082103/HBuilderX/plugins/uniapp-cli-vite/uniPage:/cGFnZXMvYWNjb3VudC9zZWxlY3QudnVl"],"sourcesContent":["<template>\r\n\t<view class=\"page\">\r\n\t\t<scroll-view scroll-y class=\"list\">\r\n\t\t\t<view class=\"item\" v-for=\"a in accounts\" :key=\"a.id\" @click=\"select(a)\">\r\n\t\t\t\t<view class=\"name\">{{ a.name }}</view>\r\n\t\t\t\t<view class=\"meta\">{{ typeLabel(a.type) }} · 余额:{{ a.balance?.toFixed ? a.balance.toFixed(2) : a.balance }}</view>\r\n\t\t\t</view>\r\n\t\t</scroll-view>\r\n\t</view>\r\n</template>\r\n\r\n<script>\r\n\timport { get } from '../../common/http.js'\r\n\tconst TYPE_MAP = { cash: '现金', bank: '银行', alipay: '支付宝', wechat: '微信', other: '其他' }\r\n\texport default {\r\n\t\tdata() { return { accounts: [] } },\r\n\t\tasync onLoad() {\r\n\t\t\ttry {\r\n\t\t\t\tconst res = await get('/api/accounts')\r\n\t\t\t\tthis.accounts = Array.isArray(res) ? res : (res?.list || [])\r\n\t\t\t} catch(e) { uni.showToast({ title: '加载失败', icon: 'none' }) }\r\n\t\t},\r\n\t\tmethods: {\r\n\t\t\tselect(a) {\r\n\t\t\t\tconst opener = getCurrentPages()[getCurrentPages().length-2]\r\n\t\t\t\tif (opener && opener.$vm) {\r\n\t\t\t\t\topener.$vm.selectedAccountId = a.id\r\n\t\t\t\t\topener.$vm.selectedAccountName = a.name\r\n\t\t\t\t}\r\n\t\t\t\tuni.navigateBack()\r\n\t\t\t},\r\n\t\t\ttypeLabel(t) { return TYPE_MAP[t] || t }\r\n\t\t}\r\n\t}\r\n</script>\r\n\r\n<style>\r\n\t.page { display:flex; flex-direction: column; height: 100vh; }\r\n\t.list { flex:1; }\r\n\t.item { padding: 20rpx 24rpx; background:#fff; border-bottom: 1rpx solid #f1f1f1; }\r\n\t.name { color:#333; margin-bottom: 6rpx; }\r\n\t.meta { color:#888; font-size: 24rpx; }\r\n</style>\r\n\r\n\r\n","import MiniProgramPage from 'C:/Users/21826/Desktop/Wj/PartsInquiry/frontend/pages/account/select.vue'\nwx.createPage(MiniProgramPage)"],"names":["get","uni"],"mappings":";;;AAaC,MAAM,WAAW,EAAE,MAAM,MAAM,MAAM,MAAM,QAAQ,OAAO,QAAQ,MAAM,OAAO,KAAK;AACpF,MAAK,YAAU;AAAA,EACd,OAAO;AAAE,WAAO,EAAE,UAAU,CAAG,EAAA;AAAA,EAAG;AAAA,EAClC,MAAM,SAAS;AACd,QAAI;AACH,YAAM,MAAM,MAAMA,YAAG,IAAC,eAAe;AACrC,WAAK,WAAW,MAAM,QAAQ,GAAG,IAAI,OAAO,2BAAK,SAAQ;aAClD,GAAG;AAAEC,oBAAAA,MAAI,UAAU,EAAE,OAAO,QAAQ,MAAM,OAAO,CAAC;AAAA,IAAE;AAAA,EAC5D;AAAA,EACD,SAAS;AAAA,IACR,OAAO,GAAG;AACT,YAAM,SAAS,gBAAiB,EAAC,gBAAe,EAAG,SAAO,CAAC;AAC3D,UAAI,UAAU,OAAO,KAAK;AACzB,eAAO,IAAI,oBAAoB,EAAE;AACjC,eAAO,IAAI,sBAAsB,EAAE;AAAA,MACpC;AACAA,oBAAAA,MAAI,aAAa;AAAA,IACjB;AAAA,IACD,UAAU,GAAG;AAAE,aAAO,SAAS,CAAC,KAAK;AAAA,IAAE;AAAA,EACxC;AACD;;;;;;;;;;;;;;;;AChCD,GAAG,WAAW,eAAe;"}
{"version":3,"file":"select.js","sources":["pages/account/select.vue","../../../../Downloads/HBuilderX.4.76.2025082103/HBuilderX/plugins/uniapp-cli-vite/uniPage:/cGFnZXMvYWNjb3VudC9zZWxlY3QudnVl"],"sourcesContent":["<template>\r\n\t<view class=\"page\">\r\n\t\t<scroll-view scroll-y class=\"list\">\r\n\t\t\t<view class=\"item\" v-for=\"a in accounts\" :key=\"a.id\" @click=\"select(a)\">\r\n\t\t\t\t<view class=\"name\">{{ a.name }}</view>\r\n\t\t\t\t<view class=\"meta\">{{ typeLabel(a.type) }} · 余额:{{ a.balance?.toFixed ? a.balance.toFixed(2) : a.balance }}</view>\r\n\t\t\t</view>\r\n\t\t</scroll-view>\r\n\t</view>\r\n</template>\r\n\r\n<script>\r\n\timport { get } from '../../common/http.js'\r\n\tconst TYPE_MAP = { cash: '现金', bank: '银行', alipay: '支付宝', wechat: '微信', other: '其他' }\r\n\texport default {\r\n\t\tdata() { return { accounts: [] } },\r\n\t\tasync onLoad() {\r\n\t\t\ttry {\r\n\t\t\t\tconst res = await get('/api/accounts')\r\n\t\t\t\tthis.accounts = Array.isArray(res) ? res : (res?.list || [])\r\n\t\t\t} catch(e) { uni.showToast({ title: '加载失败', icon: 'none' }) }\r\n\t\t},\r\n\t\tmethods: {\r\n\t\t\tselect(a) {\r\n\t\t\t\tconst opener = getCurrentPages()[getCurrentPages().length-2]\r\n\t\t\t\tif (opener && opener.$vm) {\r\n\t\t\t\t\topener.$vm.selectedAccountId = a.id\r\n\t\t\t\t\topener.$vm.selectedAccountName = a.name\r\n\t\t\t\t}\r\n\t\t\t\tuni.navigateBack()\r\n\t\t\t},\r\n\t\t\ttypeLabel(t) { return TYPE_MAP[t] || t }\r\n\t\t}\r\n\t}\r\n</script>\r\n\r\n<style>\r\n\t.page { display:flex; flex-direction: column; height: 100vh; }\r\n\t.list { flex:1; }\r\n\t.item { padding: 20rpx 24rpx; background:#fff; border-bottom: 1rpx solid #f1f1f1; }\r\n\t.name { color:#333; margin-bottom: 6rpx; }\r\n\t.meta { color:#888; font-size: 24rpx; }\r\n</style>\r\n\r\n\r\n\r\n","import MiniProgramPage from 'C:/Users/21826/Desktop/Wj/PartsInquiry/frontend/pages/account/select.vue'\nwx.createPage(MiniProgramPage)"],"names":["get","uni"],"mappings":";;;AAaC,MAAM,WAAW,EAAE,MAAM,MAAM,MAAM,MAAM,QAAQ,OAAO,QAAQ,MAAM,OAAO,KAAK;AACpF,MAAK,YAAU;AAAA,EACd,OAAO;AAAE,WAAO,EAAE,UAAU,CAAG,EAAA;AAAA,EAAG;AAAA,EAClC,MAAM,SAAS;AACd,QAAI;AACH,YAAM,MAAM,MAAMA,YAAG,IAAC,eAAe;AACrC,WAAK,WAAW,MAAM,QAAQ,GAAG,IAAI,OAAO,2BAAK,SAAQ;aAClD,GAAG;AAAEC,oBAAAA,MAAI,UAAU,EAAE,OAAO,QAAQ,MAAM,OAAO,CAAC;AAAA,IAAE;AAAA,EAC5D;AAAA,EACD,SAAS;AAAA,IACR,OAAO,GAAG;AACT,YAAM,SAAS,gBAAiB,EAAC,gBAAe,EAAG,SAAO,CAAC;AAC3D,UAAI,UAAU,OAAO,KAAK;AACzB,eAAO,IAAI,oBAAoB,EAAE;AACjC,eAAO,IAAI,sBAAsB,EAAE;AAAA,MACpC;AACAA,oBAAAA,MAAI,aAAa;AAAA,IACjB;AAAA,IACD,UAAU,GAAG;AAAE,aAAO,SAAS,CAAC,KAAK;AAAA,IAAE;AAAA,EACxC;AACD;;;;;;;;;;;;;;;;AChCD,GAAG,WAAW,eAAe;"}

View File

@@ -1 +1 @@
{"version":3,"file":"select.js","sources":["pages/customer/select.vue","../../../../Downloads/HBuilderX.4.76.2025082103/HBuilderX/plugins/uniapp-cli-vite/uniPage:/cGFnZXMvY3VzdG9tZXIvc2VsZWN0LnZ1ZQ"],"sourcesContent":["<template>\r\n\t<view class=\"page\">\r\n\t\t<view class=\"search\">\r\n\t\t\t<input v-model=\"kw\" placeholder=\"搜索客户名称/电话\" @confirm=\"search\" />\r\n\t\t\t<button size=\"mini\" @click=\"search\">搜索</button>\r\n\t\t</view>\r\n\t\t<scroll-view scroll-y class=\"list\">\r\n\t\t\t<view class=\"item\" v-for=\"c in customers\" :key=\"c.id\" @click=\"select(c)\">\r\n\t\t\t\t<view class=\"name\">{{ c.name }}</view>\r\n\t\t\t\t<view class=\"meta\">{{ c.mobile || '—' }}</view>\r\n\t\t\t</view>\r\n\t\t</scroll-view>\r\n\t</view>\r\n</template>\r\n\r\n<script>\r\n\timport { get } from '../../common/http.js'\r\n\texport default {\r\n\t\tdata() { return { kw: '', customers: [] } },\r\n\t\tonLoad() { this.search() },\r\n\t\tmethods: {\r\n\t\t\tasync search() {\r\n\t\t\t\ttry {\r\n\t\t\t\t\tconst res = await get('/api/customers', { kw: this.kw, page: 1, size: 50 })\r\n\t\t\t\t\tthis.customers = Array.isArray(res?.list) ? res.list : (Array.isArray(res) ? res : [])\r\n\t\t\t\t} catch(e) { uni.showToast({ title: '加载失败', icon: 'none' }) }\r\n\t\t\t},\r\n\t\t\tselect(c) {\r\n\t\t\t\tconst opener = getCurrentPages()[getCurrentPages().length-2]\r\n\t\t\t\tif (opener && opener.$vm) {\r\n\t\t\t\t\topener.$vm.order.customerId = c.id\r\n\t\t\t\t\topener.$vm.customerName = c.name\r\n\t\t\t\t}\r\n\t\t\t\tuni.navigateBack()\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n</script>\r\n\r\n<style>\r\n\t.page { display:flex; flex-direction: column; height: 100vh; }\r\n\t.search { display:flex; gap: 12rpx; padding: 16rpx; background:#fff; }\r\n\t.search input { flex:1; background:#f6f6f6; border-radius: 12rpx; padding: 12rpx; }\r\n\t.list { flex:1; }\r\n\t.item { padding: 20rpx 24rpx; background:#fff; border-bottom: 1rpx solid #f1f1f1; }\r\n\t.name { color:#333; margin-bottom: 6rpx; }\r\n\t.meta { color:#888; font-size: 24rpx; }\r\n</style>\r\n\r\n\r\n","import MiniProgramPage from 'C:/Users/21826/Desktop/Wj/PartsInquiry/frontend/pages/customer/select.vue'\nwx.createPage(MiniProgramPage)"],"names":["get","uni"],"mappings":";;;AAiBC,MAAK,YAAU;AAAA,EACd,OAAO;AAAE,WAAO,EAAE,IAAI,IAAI,WAAW,CAAA;EAAM;AAAA,EAC3C,SAAS;AAAE,SAAK;EAAU;AAAA,EAC1B,SAAS;AAAA,IACR,MAAM,SAAS;AACd,UAAI;AACH,cAAM,MAAM,MAAMA,gBAAI,kBAAkB,EAAE,IAAI,KAAK,IAAI,MAAM,GAAG,MAAM,GAAC,CAAG;AAC1E,aAAK,YAAY,MAAM,QAAQ,2BAAK,IAAI,IAAI,IAAI,OAAQ,MAAM,QAAQ,GAAG,IAAI,MAAM,CAAA;AAAA,eAC5E,GAAG;AAAEC,sBAAAA,MAAI,UAAU,EAAE,OAAO,QAAQ,MAAM,OAAO,CAAC;AAAA,MAAE;AAAA,IAC5D;AAAA,IACD,OAAO,GAAG;AACT,YAAM,SAAS,gBAAiB,EAAC,gBAAe,EAAG,SAAO,CAAC;AAC3D,UAAI,UAAU,OAAO,KAAK;AACzB,eAAO,IAAI,MAAM,aAAa,EAAE;AAChC,eAAO,IAAI,eAAe,EAAE;AAAA,MAC7B;AACAA,oBAAAA,MAAI,aAAa;AAAA,IAClB;AAAA,EACD;AACD;;;;;;;;;;;;;;;;;;ACnCD,GAAG,WAAW,eAAe;"}
{"version":3,"file":"select.js","sources":["pages/customer/select.vue","../../../../Downloads/HBuilderX.4.76.2025082103/HBuilderX/plugins/uniapp-cli-vite/uniPage:/cGFnZXMvY3VzdG9tZXIvc2VsZWN0LnZ1ZQ"],"sourcesContent":["<template>\r\n\t<view class=\"page\">\r\n\t\t<view class=\"search\">\r\n\t\t\t<input v-model=\"kw\" placeholder=\"搜索客户名称/电话\" @confirm=\"search\" />\r\n\t\t\t<button size=\"mini\" @click=\"search\">搜索</button>\r\n\t\t</view>\r\n\t\t<scroll-view scroll-y class=\"list\">\r\n\t\t\t<view class=\"item\" v-for=\"c in customers\" :key=\"c.id\" @click=\"select(c)\">\r\n\t\t\t\t<view class=\"name\">{{ c.name }}</view>\r\n\t\t\t\t<view class=\"meta\">{{ c.mobile || '—' }}</view>\r\n\t\t\t</view>\r\n\t\t</scroll-view>\r\n\t</view>\r\n</template>\r\n\r\n<script>\r\n\timport { get } from '../../common/http.js'\r\n\texport default {\r\n\t\tdata() { return { kw: '', customers: [] } },\r\n\t\tonLoad() { this.search() },\r\n\t\tmethods: {\r\n\t\t\tasync search() {\r\n\t\t\t\ttry {\r\n\t\t\t\t\tconst res = await get('/api/customers', { kw: this.kw, page: 1, size: 50 })\r\n\t\t\t\t\tthis.customers = Array.isArray(res?.list) ? res.list : (Array.isArray(res) ? res : [])\r\n\t\t\t\t} catch(e) { uni.showToast({ title: '加载失败', icon: 'none' }) }\r\n\t\t\t},\r\n\t\t\tselect(c) {\r\n\t\t\t\tconst opener = getCurrentPages()[getCurrentPages().length-2]\r\n\t\t\t\tif (opener && opener.$vm) {\r\n\t\t\t\t\topener.$vm.order.customerId = c.id\r\n\t\t\t\t\topener.$vm.customerName = c.name\r\n\t\t\t\t}\r\n\t\t\t\tuni.navigateBack()\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n</script>\r\n\r\n<style>\r\n\t.page { display:flex; flex-direction: column; height: 100vh; }\r\n\t.search { display:flex; gap: 12rpx; padding: 16rpx; background:#fff; }\r\n\t.search input { flex:1; background:#f6f6f6; border-radius: 12rpx; padding: 12rpx; }\r\n\t.list { flex:1; }\r\n\t.item { padding: 20rpx 24rpx; background:#fff; border-bottom: 1rpx solid #f1f1f1; }\r\n\t.name { color:#333; margin-bottom: 6rpx; }\r\n\t.meta { color:#888; font-size: 24rpx; }\r\n</style>\r\n\r\n\r\n\r\n","import MiniProgramPage from 'C:/Users/21826/Desktop/Wj/PartsInquiry/frontend/pages/customer/select.vue'\nwx.createPage(MiniProgramPage)"],"names":["get","uni"],"mappings":";;;AAiBC,MAAK,YAAU;AAAA,EACd,OAAO;AAAE,WAAO,EAAE,IAAI,IAAI,WAAW,CAAA;EAAM;AAAA,EAC3C,SAAS;AAAE,SAAK;EAAU;AAAA,EAC1B,SAAS;AAAA,IACR,MAAM,SAAS;AACd,UAAI;AACH,cAAM,MAAM,MAAMA,gBAAI,kBAAkB,EAAE,IAAI,KAAK,IAAI,MAAM,GAAG,MAAM,GAAC,CAAG;AAC1E,aAAK,YAAY,MAAM,QAAQ,2BAAK,IAAI,IAAI,IAAI,OAAQ,MAAM,QAAQ,GAAG,IAAI,MAAM,CAAA;AAAA,eAC5E,GAAG;AAAEC,sBAAAA,MAAI,UAAU,EAAE,OAAO,QAAQ,MAAM,OAAO,CAAC;AAAA,MAAE;AAAA,IAC5D;AAAA,IACD,OAAO,GAAG;AACT,YAAM,SAAS,gBAAiB,EAAC,gBAAe,EAAG,SAAO,CAAC;AAC3D,UAAI,UAAU,OAAO,KAAK;AACzB,eAAO,IAAI,MAAM,aAAa,EAAE;AAChC,eAAO,IAAI,eAAe,EAAE;AAAA,MAC7B;AACAA,oBAAAA,MAAI,aAAa;AAAA,IAClB;AAAA,EACD;AACD;;;;;;;;;;;;;;;;;;ACnCD,GAAG,WAAW,eAAe;"}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
{"version":3,"file":"categories.js","sources":["pages/product/categories.vue","../../../../Downloads/HBuilderX.4.76.2025082103/HBuilderX/plugins/uniapp-cli-vite/uniPage:/cGFnZXMvcHJvZHVjdC9jYXRlZ29yaWVzLnZ1ZQ"],"sourcesContent":["<template>\r\n\t<view class=\"page\">\r\n\t\t<view class=\"toolbar\">\r\n\t\t\t<input v-model.trim=\"name\" placeholder=\"新类别名称\" />\r\n\t\t\t<button size=\"mini\" @click=\"create\">新增</button>\r\n\t\t</view>\r\n\t\t<scroll-view scroll-y class=\"list\">\r\n\t\t\t<view class=\"item\" v-for=\"c in list\" :key=\"c.id\">\r\n\t\t\t\t<input v-model.trim=\"c.name\" />\r\n\t\t\t\t<view class=\"ops\">\r\n\t\t\t\t\t<button size=\"mini\" @click=\"update(c)\">保存</button>\r\n\t\t\t\t\t<button size=\"mini\" type=\"warn\" @click=\"remove(c)\">删除</button>\r\n\t\t\t\t</view>\r\n\t\t\t</view>\r\n\t\t</scroll-view>\r\n\t</view>\r\n</template>\r\n\r\n<script>\r\nimport { get, post, put, del } from '../../common/http.js'\r\n\r\nexport default {\r\n\tdata() {\r\n\t\treturn { name: '', list: [] }\r\n\t},\r\n\tonLoad() { this.reload() },\r\n\tmethods: {\r\n\t\tasync reload() {\r\n\t\t\ttry {\r\n\t\t\t\tconst res = await get('/api/product-categories')\r\n\t\t\t\tthis.list = Array.isArray(res?.list) ? res.list : (Array.isArray(res) ? res : [])\r\n\t\t\t} catch (_) {}\r\n\t\t},\r\n\t\tasync create() {\r\n\t\t\tif (!this.name) return\r\n\t\t\tawait post('/api/product-categories', { name: this.name })\r\n\t\t\tthis.name = ''\r\n\t\t\tthis.reload()\r\n\t\t},\r\n\t\tasync update(c) {\r\n\t\t\tawait put('/api/product-categories/' + c.id, { name: c.name })\r\n\t\t\tuni.showToast({ title: '已保存', icon: 'success' })\r\n\t\t},\r\n\t\tasync remove(c) {\r\n\t\t\tuni.showModal({ content: '确定删除该类别?', success: async (r) => {\r\n\t\t\t\tif (!r.confirm) return\r\n\t\t\t\tawait del('/api/product-categories/' + c.id)\r\n\t\t\t\tthis.reload()\r\n\t\t\t}})\r\n\t\t}\r\n\t}\r\n}\r\n</script>\r\n\r\n<style>\r\n.page { display:flex; flex-direction: column; height: 100vh; }\r\n.toolbar { display:flex; gap: 12rpx; padding: 16rpx; background:#fff; }\r\n.toolbar input { flex:1; background:#f6f6f6; border-radius: 12rpx; padding: 12rpx; }\r\n.list { flex:1; }\r\n.item { display:flex; gap: 12rpx; align-items:center; padding: 16rpx; background:#fff; border-bottom: 1rpx solid #f1f1f1; }\r\n.item input { flex:1; background:#f7f7f7; border-radius: 10rpx; padding: 12rpx; }\r\n.ops { display:flex; gap: 10rpx; }\r\n</style>\r\n\r\n\r\n\r\n\r\n","import MiniProgramPage from 'C:/Users/21826/Desktop/Wj/PartsInquiry/frontend/pages/product/categories.vue'\nwx.createPage(MiniProgramPage)"],"names":["get","post","put","uni","del"],"mappings":";;;AAqBA,MAAK,YAAU;AAAA,EACd,OAAO;AACN,WAAO,EAAE,MAAM,IAAI,MAAM,CAAA,EAAG;AAAA,EAC5B;AAAA,EACD,SAAS;AAAE,SAAK;EAAU;AAAA,EAC1B,SAAS;AAAA,IACR,MAAM,SAAS;AACd,UAAI;AACH,cAAM,MAAM,MAAMA,YAAG,IAAC,yBAAyB;AAC/C,aAAK,OAAO,MAAM,QAAQ,2BAAK,IAAI,IAAI,IAAI,OAAQ,MAAM,QAAQ,GAAG,IAAI,MAAM,CAAA;AAAA,eACtE,GAAG;AAAA,MAAC;AAAA,IACb;AAAA,IACD,MAAM,SAAS;AACd,UAAI,CAAC,KAAK;AAAM;AAChB,YAAMC,YAAAA,KAAK,2BAA2B,EAAE,MAAM,KAAK,KAAG,CAAG;AACzD,WAAK,OAAO;AACZ,WAAK,OAAO;AAAA,IACZ;AAAA,IACD,MAAM,OAAO,GAAG;AACf,YAAMC,YAAG,IAAC,6BAA6B,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM;AAC7DC,oBAAG,MAAC,UAAU,EAAE,OAAO,OAAO,MAAM,WAAW;AAAA,IAC/C;AAAA,IACD,MAAM,OAAO,GAAG;AACfA,oBAAG,MAAC,UAAU,EAAE,SAAS,YAAY,SAAS,OAAO,MAAM;AAC1D,YAAI,CAAC,EAAE;AAAS;AAChB,cAAMC,gBAAI,6BAA6B,EAAE,EAAE;AAC3C,aAAK,OAAO;AAAA,MACb,EAAC,CAAC;AAAA,IACH;AAAA,EACD;AACD;;;;;;;;;;;;;;;;;;;;;;AClDA,GAAG,WAAW,eAAe;"}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
{"version":3,"file":"select.js","sources":["pages/product/select.vue","../../../../Downloads/HBuilderX.4.76.2025082103/HBuilderX/plugins/uniapp-cli-vite/uniPage:/cGFnZXMvcHJvZHVjdC9zZWxlY3QudnVl"],"sourcesContent":["<template>\r\n\t<view class=\"page\">\r\n\t\t<view class=\"search\">\r\n\t\t\t<input v-model=\"kw\" placeholder=\"搜索商品名称/编码\" @confirm=\"search\" />\r\n\t\t\t<button size=\"mini\" @click=\"search\">搜索</button>\r\n\t\t</view>\r\n\t\t<scroll-view scroll-y class=\"list\">\r\n\t\t\t<view class=\"item\" v-for=\"p in products\" :key=\"p.id\" @click=\"select(p)\">\r\n\t\t\t\t<view class=\"name\">{{ p.name }}</view>\r\n\t\t\t\t<view class=\"meta\">{{ p.code }} · 库存:{{ p.stock || 0 }}</view>\r\n\t\t\t</view>\r\n\t\t</scroll-view>\r\n\t</view>\r\n</template>\r\n\r\n<script>\r\n\timport { get } from '../../common/http.js'\r\n\texport default {\r\n\t\tdata() { return { kw: '', products: [] } },\r\n\t\tonLoad() { this.search() },\r\n\t\tmethods: {\r\n\t\t\tasync search() {\r\n\t\t\t\ttry {\r\n\t\t\t\t\tconst res = await get('/api/products', { kw: this.kw, page: 1, size: 50 })\r\n\t\t\t\t\tthis.products = Array.isArray(res?.list) ? res.list : (Array.isArray(res) ? res : [])\r\n\t\t\t\t} catch(e) { uni.showToast({ title: '加载失败', icon: 'none' }) }\r\n\t\t\t},\r\n\t\t\tselect(p) {\r\n\t\t\t\tconst opener = getCurrentPages()[getCurrentPages().length-2]\r\n\t\t\t\tif (opener && opener.$vm && opener.$vm.items) {\r\n\t\t\t\t\topener.$vm.items.push({ productId: p.id, productName: p.name, quantity: 1, unitPrice: Number(p.price || 0) })\r\n\t\t\t\t}\r\n\t\t\t\tuni.navigateBack()\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n</script>\r\n\r\n<style>\r\n\t.page { display:flex; flex-direction: column; height: 100vh; }\r\n\t.search { display:flex; gap: 12rpx; padding: 16rpx; background:#fff; }\r\n\t.search input { flex:1; background:#f6f6f6; border-radius: 12rpx; padding: 12rpx; }\r\n\t.list { flex:1; }\r\n\t.item { padding: 20rpx 24rpx; background:#fff; border-bottom: 1rpx solid #f1f1f1; }\r\n\t.name { color:#333; margin-bottom: 6rpx; }\r\n\t.meta { color:#888; font-size: 24rpx; }\r\n</style>\r\n\r\n\r\n","import MiniProgramPage from 'C:/Users/21826/Desktop/Wj/PartsInquiry/frontend/pages/product/select.vue'\nwx.createPage(MiniProgramPage)"],"names":["get","uni"],"mappings":";;;AAiBC,MAAK,YAAU;AAAA,EACd,OAAO;AAAE,WAAO,EAAE,IAAI,IAAI,UAAU,CAAA;EAAM;AAAA,EAC1C,SAAS;AAAE,SAAK;EAAU;AAAA,EAC1B,SAAS;AAAA,IACR,MAAM,SAAS;AACd,UAAI;AACH,cAAM,MAAM,MAAMA,gBAAI,iBAAiB,EAAE,IAAI,KAAK,IAAI,MAAM,GAAG,MAAM,GAAC,CAAG;AACzE,aAAK,WAAW,MAAM,QAAQ,2BAAK,IAAI,IAAI,IAAI,OAAQ,MAAM,QAAQ,GAAG,IAAI,MAAM,CAAA;AAAA,eAC3E,GAAG;AAAEC,sBAAAA,MAAI,UAAU,EAAE,OAAO,QAAQ,MAAM,OAAO,CAAC;AAAA,MAAE;AAAA,IAC5D;AAAA,IACD,OAAO,GAAG;AACT,YAAM,SAAS,gBAAiB,EAAC,gBAAe,EAAG,SAAO,CAAC;AAC3D,UAAI,UAAU,OAAO,OAAO,OAAO,IAAI,OAAO;AAC7C,eAAO,IAAI,MAAM,KAAK,EAAE,WAAW,EAAE,IAAI,aAAa,EAAE,MAAM,UAAU,GAAG,WAAW,OAAO,EAAE,SAAS,CAAC,GAAG;AAAA,MAC7G;AACAA,oBAAAA,MAAI,aAAa;AAAA,IAClB;AAAA,EACD;AACD;;;;;;;;;;;;;;;;;;;AClCD,GAAG,WAAW,eAAe;"}
{"version":3,"file":"select.js","sources":["pages/product/select.vue","../../../../Downloads/HBuilderX.4.76.2025082103/HBuilderX/plugins/uniapp-cli-vite/uniPage:/cGFnZXMvcHJvZHVjdC9zZWxlY3QudnVl"],"sourcesContent":["<template>\r\n\t<view class=\"page\">\r\n\t\t<view class=\"search\">\r\n\t\t\t<input v-model=\"kw\" placeholder=\"搜索商品名称/编码\" @confirm=\"search\" />\r\n\t\t\t<button size=\"mini\" @click=\"search\">搜索</button>\r\n\t\t</view>\r\n\t\t<scroll-view scroll-y class=\"list\">\r\n\t\t\t<view class=\"item\" v-for=\"p in products\" :key=\"p.id\" @click=\"select(p)\">\r\n\t\t\t\t<view class=\"name\">{{ p.name }}</view>\r\n\t\t\t\t<view class=\"meta\">{{ p.code }} · 库存:{{ p.stock || 0 }}</view>\r\n\t\t\t</view>\r\n\t\t</scroll-view>\r\n\t</view>\r\n</template>\r\n\r\n<script>\r\n\timport { get } from '../../common/http.js'\r\n\texport default {\r\n\t\tdata() { return { kw: '', products: [] } },\r\n\t\tonLoad() { this.search() },\r\n\t\tmethods: {\r\n\t\t\tasync search() {\r\n\t\t\t\ttry {\r\n\t\t\t\t\tconst res = await get('/api/products', { kw: this.kw, page: 1, size: 50 })\r\n\t\t\t\t\tthis.products = Array.isArray(res?.list) ? res.list : (Array.isArray(res) ? res : [])\r\n\t\t\t\t} catch(e) { uni.showToast({ title: '加载失败', icon: 'none' }) }\r\n\t\t\t},\r\n\t\t\tselect(p) {\r\n\t\t\t\tconst opener = getCurrentPages()[getCurrentPages().length-2]\r\n\t\t\t\tif (opener && opener.$vm && opener.$vm.items) {\r\n\t\t\t\t\topener.$vm.items.push({ productId: p.id, productName: p.name, quantity: 1, unitPrice: Number(p.price || 0) })\r\n\t\t\t\t}\r\n\t\t\t\tuni.navigateBack()\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n</script>\r\n\r\n<style>\r\n\t.page { display:flex; flex-direction: column; height: 100vh; }\r\n\t.search { display:flex; gap: 12rpx; padding: 16rpx; background:#fff; }\r\n\t.search input { flex:1; background:#f6f6f6; border-radius: 12rpx; padding: 12rpx; }\r\n\t.list { flex:1; }\r\n\t.item { padding: 20rpx 24rpx; background:#fff; border-bottom: 1rpx solid #f1f1f1; }\r\n\t.name { color:#333; margin-bottom: 6rpx; }\r\n\t.meta { color:#888; font-size: 24rpx; }\r\n</style>\r\n\r\n\r\n\r\n","import MiniProgramPage from 'C:/Users/21826/Desktop/Wj/PartsInquiry/frontend/pages/product/select.vue'\nwx.createPage(MiniProgramPage)"],"names":["get","uni"],"mappings":";;;AAiBC,MAAK,YAAU;AAAA,EACd,OAAO;AAAE,WAAO,EAAE,IAAI,IAAI,UAAU,CAAA;EAAM;AAAA,EAC1C,SAAS;AAAE,SAAK;EAAU;AAAA,EAC1B,SAAS;AAAA,IACR,MAAM,SAAS;AACd,UAAI;AACH,cAAM,MAAM,MAAMA,gBAAI,iBAAiB,EAAE,IAAI,KAAK,IAAI,MAAM,GAAG,MAAM,GAAC,CAAG;AACzE,aAAK,WAAW,MAAM,QAAQ,2BAAK,IAAI,IAAI,IAAI,OAAQ,MAAM,QAAQ,GAAG,IAAI,MAAM,CAAA;AAAA,eAC3E,GAAG;AAAEC,sBAAAA,MAAI,UAAU,EAAE,OAAO,QAAQ,MAAM,OAAO,CAAC;AAAA,MAAE;AAAA,IAC5D;AAAA,IACD,OAAO,GAAG;AACT,YAAM,SAAS,gBAAiB,EAAC,gBAAe,EAAG,SAAO,CAAC;AAC3D,UAAI,UAAU,OAAO,OAAO,OAAO,IAAI,OAAO;AAC7C,eAAO,IAAI,MAAM,KAAK,EAAE,WAAW,EAAE,IAAI,aAAa,EAAE,MAAM,UAAU,GAAG,WAAW,OAAO,EAAE,SAAS,CAAC,GAAG;AAAA,MAC7G;AACAA,oBAAAA,MAAI,aAAa;AAAA,IAClB;AAAA,EACD;AACD;;;;;;;;;;;;;;;;;;;AClCD,GAAG,WAAW,eAAe;"}

View File

@@ -0,0 +1 @@
{"version":3,"file":"settings.js","sources":["pages/product/settings.vue","../../../../Downloads/HBuilderX.4.76.2025082103/HBuilderX/plugins/uniapp-cli-vite/uniPage:/cGFnZXMvcHJvZHVjdC9zZXR0aW5ncy52dWU"],"sourcesContent":["<template>\r\n\t<view class=\"page\">\r\n\t\t<view class=\"item\">\r\n\t\t\t<text>隐藏零库存商品</text>\r\n\t\t\t<switch :checked=\"settings.hideZeroStock\" @change=\"(e)=>update('hideZeroStock', e.detail.value)\" />\r\n\t\t</view>\r\n\t\t<view class=\"item\">\r\n\t\t\t<text>隐藏进货价</text>\r\n\t\t\t<switch :checked=\"settings.hidePurchasePrice\" @change=\"(e)=>update('hidePurchasePrice', e.detail.value)\" />\r\n\t\t</view>\r\n\t</view>\r\n</template>\r\n\r\n<script>\r\nimport { get, put } from '../../common/http.js'\r\n\r\nexport default {\r\n\tdata() {\r\n\t\treturn { settings: { hideZeroStock: false, hidePurchasePrice: false } }\r\n\t},\r\n\tonLoad() { this.load() },\r\n\tmethods: {\r\n\t\tasync load() {\r\n\t\t\ttry {\r\n\t\t\t\tconst res = await get('/api/product-settings')\r\n\t\t\t\tthis.settings = { hideZeroStock: !!res?.hideZeroStock, hidePurchasePrice: !!res?.hidePurchasePrice }\r\n\t\t\t} catch (_) {}\r\n\t\t},\r\n\t\tasync update(key, val) {\r\n\t\t\tconst next = { ...this.settings, [key]: val }\r\n\t\t\tthis.settings = next\r\n\t\t\ttry { await put('/api/product-settings', next) } catch (_) {}\r\n\t\t}\r\n\t}\r\n}\r\n</script>\r\n\r\n<style>\r\n.page { background:#fff; }\r\n.item { display:flex; justify-content: space-between; align-items:center; padding: 20rpx; border-bottom: 1rpx solid #f1f1f1; }\r\n</style>\r\n\r\n\r\n\r\n\r\n","import MiniProgramPage from 'C:/Users/21826/Desktop/Wj/PartsInquiry/frontend/pages/product/settings.vue'\nwx.createPage(MiniProgramPage)"],"names":["get","put"],"mappings":";;;AAgBA,MAAK,YAAU;AAAA,EACd,OAAO;AACN,WAAO,EAAE,UAAU,EAAE,eAAe,OAAO,mBAAmB,QAAQ;AAAA,EACtE;AAAA,EACD,SAAS;AAAE,SAAK;EAAQ;AAAA,EACxB,SAAS;AAAA,IACR,MAAM,OAAO;AACZ,UAAI;AACH,cAAM,MAAM,MAAMA,YAAG,IAAC,uBAAuB;AAC7C,aAAK,WAAW,EAAE,eAAe,CAAC,EAAC,2BAAK,gBAAe,mBAAmB,CAAC,EAAC,2BAAK,mBAAkB;AAAA,eAC3F,GAAG;AAAA,MAAC;AAAA,IACb;AAAA,IACD,MAAM,OAAO,KAAK,KAAK;AACtB,YAAM,OAAO,EAAE,GAAG,KAAK,UAAU,CAAC,GAAG,GAAG,IAAI;AAC5C,WAAK,WAAW;AAChB,UAAI;AAAE,cAAMC,YAAAA,IAAI,yBAAyB,IAAI;AAAA,MAAI,SAAO,GAAG;AAAA,MAAC;AAAA,IAC7D;AAAA,EACD;AACD;;;;;;;;;;ACjCA,GAAG,WAAW,eAAe;"}

View File

@@ -0,0 +1 @@
{"version":3,"file":"units.js","sources":["pages/product/units.vue","../../../../Downloads/HBuilderX.4.76.2025082103/HBuilderX/plugins/uniapp-cli-vite/uniPage:/cGFnZXMvcHJvZHVjdC91bml0cy52dWU"],"sourcesContent":["<template>\r\n\t<view class=\"page\">\r\n\t\t<view class=\"toolbar\">\r\n\t\t\t<input v-model.trim=\"name\" placeholder=\"新单位名称\" />\r\n\t\t\t<button size=\"mini\" @click=\"create\">新增</button>\r\n\t\t</view>\r\n\t\t<scroll-view scroll-y class=\"list\">\r\n\t\t\t<view class=\"item\" v-for=\"u in list\" :key=\"u.id\">\r\n\t\t\t\t<input v-model.trim=\"u.name\" />\r\n\t\t\t\t<view class=\"ops\">\r\n\t\t\t\t\t<button size=\"mini\" @click=\"update(u)\">保存</button>\r\n\t\t\t\t\t<button size=\"mini\" type=\"warn\" @click=\"remove(u)\">删除</button>\r\n\t\t\t\t</view>\r\n\t\t\t</view>\r\n\t\t</scroll-view>\r\n\t</view>\r\n</template>\r\n\r\n<script>\r\nimport { get, post, put, del } from '../../common/http.js'\r\n\r\nexport default {\r\n\tdata() {\r\n\t\treturn { name: '', list: [] }\r\n\t},\r\n\tonLoad() { this.reload() },\r\n\tmethods: {\r\n\t\tasync reload() {\r\n\t\t\ttry {\r\n\t\t\t\tconst res = await get('/api/product-units')\r\n\t\t\t\tthis.list = Array.isArray(res?.list) ? res.list : (Array.isArray(res) ? res : [])\r\n\t\t\t} catch (_) {}\r\n\t\t},\r\n\t\tasync create() {\r\n\t\t\tif (!this.name) return\r\n\t\t\tawait post('/api/product-units', { name: this.name })\r\n\t\t\tthis.name = ''\r\n\t\t\tthis.reload()\r\n\t\t},\r\n\t\tasync update(u) {\r\n\t\t\tawait put('/api/product-units/' + u.id, { name: u.name })\r\n\t\t\tuni.showToast({ title: '已保存', icon: 'success' })\r\n\t\t},\r\n\t\tasync remove(u) {\r\n\t\t\tuni.showModal({ content: '确定删除该单位?', success: async (r) => {\r\n\t\t\t\tif (!r.confirm) return\r\n\t\t\t\tawait del('/api/product-units/' + u.id)\r\n\t\t\t\tthis.reload()\r\n\t\t\t}})\r\n\t\t}\r\n\t}\r\n}\r\n</script>\r\n\r\n<style>\r\n.page { display:flex; flex-direction: column; height: 100vh; }\r\n.toolbar { display:flex; gap: 12rpx; padding: 16rpx; background:#fff; }\r\n.toolbar input { flex:1; background:#f6f6f6; border-radius: 12rpx; padding: 12rpx; }\r\n.list { flex:1; }\r\n.item { display:flex; gap: 12rpx; align-items:center; padding: 16rpx; background:#fff; border-bottom: 1rpx solid #f1f1f1; }\r\n.item input { flex:1; background:#f7f7f7; border-radius: 10rpx; padding: 12rpx; }\r\n.ops { display:flex; gap: 10rpx; }\r\n</style>\r\n\r\n\r\n\r\n\r\n","import MiniProgramPage from 'C:/Users/21826/Desktop/Wj/PartsInquiry/frontend/pages/product/units.vue'\nwx.createPage(MiniProgramPage)"],"names":["get","post","put","uni","del"],"mappings":";;;AAqBA,MAAK,YAAU;AAAA,EACd,OAAO;AACN,WAAO,EAAE,MAAM,IAAI,MAAM,CAAA,EAAG;AAAA,EAC5B;AAAA,EACD,SAAS;AAAE,SAAK;EAAU;AAAA,EAC1B,SAAS;AAAA,IACR,MAAM,SAAS;AACd,UAAI;AACH,cAAM,MAAM,MAAMA,YAAG,IAAC,oBAAoB;AAC1C,aAAK,OAAO,MAAM,QAAQ,2BAAK,IAAI,IAAI,IAAI,OAAQ,MAAM,QAAQ,GAAG,IAAI,MAAM,CAAA;AAAA,eACtE,GAAG;AAAA,MAAC;AAAA,IACb;AAAA,IACD,MAAM,SAAS;AACd,UAAI,CAAC,KAAK;AAAM;AAChB,YAAMC,YAAAA,KAAK,sBAAsB,EAAE,MAAM,KAAK,KAAG,CAAG;AACpD,WAAK,OAAO;AACZ,WAAK,OAAO;AAAA,IACZ;AAAA,IACD,MAAM,OAAO,GAAG;AACf,YAAMC,YAAG,IAAC,wBAAwB,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM;AACxDC,oBAAG,MAAC,UAAU,EAAE,OAAO,OAAO,MAAM,WAAW;AAAA,IAC/C;AAAA,IACD,MAAM,OAAO,GAAG;AACfA,oBAAG,MAAC,UAAU,EAAE,SAAS,YAAY,SAAS,OAAO,MAAM;AAC1D,YAAI,CAAC,EAAE;AAAS;AAChB,cAAMC,gBAAI,wBAAwB,EAAE,EAAE;AACtC,aAAK,OAAO;AAAA,MACb,EAAC,CAAC;AAAA,IACH;AAAA,EACD;AACD;;;;;;;;;;;;;;;;;;;;;;AClDA,GAAG,WAAW,eAAe;"}

View File

@@ -1 +1 @@
{"version":3,"file":"select.js","sources":["pages/supplier/select.vue","../../../../Downloads/HBuilderX.4.76.2025082103/HBuilderX/plugins/uniapp-cli-vite/uniPage:/cGFnZXMvc3VwcGxpZXIvc2VsZWN0LnZ1ZQ"],"sourcesContent":["<template>\r\n\t<view class=\"page\">\r\n\t\t<view class=\"search\">\r\n\t\t\t<input v-model=\"kw\" placeholder=\"搜索供应商名称/电话\" @confirm=\"search\" />\r\n\t\t\t<button size=\"mini\" @click=\"search\">搜索</button>\r\n\t\t</view>\r\n\t\t<scroll-view scroll-y class=\"list\">\r\n\t\t\t<view class=\"item\" v-for=\"s in suppliers\" :key=\"s.id\" @click=\"select(s)\">\r\n\t\t\t\t<view class=\"name\">{{ s.name }}</view>\r\n\t\t\t\t<view class=\"meta\">{{ s.mobile || '—' }}</view>\r\n\t\t\t</view>\r\n\t\t</scroll-view>\r\n\t</view>\r\n</template>\r\n\r\n<script>\r\n\timport { get } from '../../common/http.js'\r\n\texport default {\r\n\t\tdata() { return { kw: '', suppliers: [] } },\r\n\t\tonLoad() { this.search() },\r\n\t\tmethods: {\r\n\t\t\tasync search() {\r\n\t\t\t\ttry {\r\n\t\t\t\t\tconst res = await get('/api/suppliers', { kw: this.kw, page: 1, size: 50 })\r\n\t\t\t\t\tthis.suppliers = Array.isArray(res?.list) ? res.list : (Array.isArray(res) ? res : [])\r\n\t\t\t\t} catch(e) { uni.showToast({ title: '加载失败', icon: 'none' }) }\r\n\t\t\t},\r\n\t\t\tselect(s) {\r\n\t\t\t\tconst opener = getCurrentPages()[getCurrentPages().length-2]\r\n\t\t\t\tif (opener && opener.$vm) {\r\n\t\t\t\t\topener.$vm.order.supplierId = s.id\r\n\t\t\t\t\topener.$vm.supplierName = s.name\r\n\t\t\t\t}\r\n\t\t\t\tuni.navigateBack()\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n</script>\r\n\r\n<style>\r\n\t.page { display:flex; flex-direction: column; height: 100vh; }\r\n\t.search { display:flex; gap: 12rpx; padding: 16rpx; background:#fff; }\r\n\t.search input { flex:1; background:#f6f6f6; border-radius: 12rpx; padding: 12rpx; }\r\n\t.list { flex:1; }\r\n\t.item { padding: 20rpx 24rpx; background:#fff; border-bottom: 1rpx solid #f1f1f1; }\r\n\t.name { color:#333; margin-bottom: 6rpx; }\r\n\t.meta { color:#888; font-size: 24rpx; }\r\n</style>\r\n\r\n\r\n","import MiniProgramPage from 'C:/Users/21826/Desktop/Wj/PartsInquiry/frontend/pages/supplier/select.vue'\nwx.createPage(MiniProgramPage)"],"names":["get","uni"],"mappings":";;;AAiBC,MAAK,YAAU;AAAA,EACd,OAAO;AAAE,WAAO,EAAE,IAAI,IAAI,WAAW,CAAA;EAAM;AAAA,EAC3C,SAAS;AAAE,SAAK;EAAU;AAAA,EAC1B,SAAS;AAAA,IACR,MAAM,SAAS;AACd,UAAI;AACH,cAAM,MAAM,MAAMA,gBAAI,kBAAkB,EAAE,IAAI,KAAK,IAAI,MAAM,GAAG,MAAM,GAAC,CAAG;AAC1E,aAAK,YAAY,MAAM,QAAQ,2BAAK,IAAI,IAAI,IAAI,OAAQ,MAAM,QAAQ,GAAG,IAAI,MAAM,CAAA;AAAA,eAC5E,GAAG;AAAEC,sBAAAA,MAAI,UAAU,EAAE,OAAO,QAAQ,MAAM,OAAO,CAAC;AAAA,MAAE;AAAA,IAC5D;AAAA,IACD,OAAO,GAAG;AACT,YAAM,SAAS,gBAAiB,EAAC,gBAAe,EAAG,SAAO,CAAC;AAC3D,UAAI,UAAU,OAAO,KAAK;AACzB,eAAO,IAAI,MAAM,aAAa,EAAE;AAChC,eAAO,IAAI,eAAe,EAAE;AAAA,MAC7B;AACAA,oBAAAA,MAAI,aAAa;AAAA,IAClB;AAAA,EACD;AACD;;;;;;;;;;;;;;;;;;ACnCD,GAAG,WAAW,eAAe;"}
{"version":3,"file":"select.js","sources":["pages/supplier/select.vue","../../../../Downloads/HBuilderX.4.76.2025082103/HBuilderX/plugins/uniapp-cli-vite/uniPage:/cGFnZXMvc3VwcGxpZXIvc2VsZWN0LnZ1ZQ"],"sourcesContent":["<template>\r\n\t<view class=\"page\">\r\n\t\t<view class=\"search\">\r\n\t\t\t<input v-model=\"kw\" placeholder=\"搜索供应商名称/电话\" @confirm=\"search\" />\r\n\t\t\t<button size=\"mini\" @click=\"search\">搜索</button>\r\n\t\t</view>\r\n\t\t<scroll-view scroll-y class=\"list\">\r\n\t\t\t<view class=\"item\" v-for=\"s in suppliers\" :key=\"s.id\" @click=\"select(s)\">\r\n\t\t\t\t<view class=\"name\">{{ s.name }}</view>\r\n\t\t\t\t<view class=\"meta\">{{ s.mobile || '—' }}</view>\r\n\t\t\t</view>\r\n\t\t</scroll-view>\r\n\t</view>\r\n</template>\r\n\r\n<script>\r\n\timport { get } from '../../common/http.js'\r\n\texport default {\r\n\t\tdata() { return { kw: '', suppliers: [] } },\r\n\t\tonLoad() { this.search() },\r\n\t\tmethods: {\r\n\t\t\tasync search() {\r\n\t\t\t\ttry {\r\n\t\t\t\t\tconst res = await get('/api/suppliers', { kw: this.kw, page: 1, size: 50 })\r\n\t\t\t\t\tthis.suppliers = Array.isArray(res?.list) ? res.list : (Array.isArray(res) ? res : [])\r\n\t\t\t\t} catch(e) { uni.showToast({ title: '加载失败', icon: 'none' }) }\r\n\t\t\t},\r\n\t\t\tselect(s) {\r\n\t\t\t\tconst opener = getCurrentPages()[getCurrentPages().length-2]\r\n\t\t\t\tif (opener && opener.$vm) {\r\n\t\t\t\t\topener.$vm.order.supplierId = s.id\r\n\t\t\t\t\topener.$vm.supplierName = s.name\r\n\t\t\t\t}\r\n\t\t\t\tuni.navigateBack()\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n</script>\r\n\r\n<style>\r\n\t.page { display:flex; flex-direction: column; height: 100vh; }\r\n\t.search { display:flex; gap: 12rpx; padding: 16rpx; background:#fff; }\r\n\t.search input { flex:1; background:#f6f6f6; border-radius: 12rpx; padding: 12rpx; }\r\n\t.list { flex:1; }\r\n\t.item { padding: 20rpx 24rpx; background:#fff; border-bottom: 1rpx solid #f1f1f1; }\r\n\t.name { color:#333; margin-bottom: 6rpx; }\r\n\t.meta { color:#888; font-size: 24rpx; }\r\n</style>\r\n\r\n\r\n\r\n","import MiniProgramPage from 'C:/Users/21826/Desktop/Wj/PartsInquiry/frontend/pages/supplier/select.vue'\nwx.createPage(MiniProgramPage)"],"names":["get","uni"],"mappings":";;;AAiBC,MAAK,YAAU;AAAA,EACd,OAAO;AAAE,WAAO,EAAE,IAAI,IAAI,WAAW,CAAA;EAAM;AAAA,EAC3C,SAAS;AAAE,SAAK;EAAU;AAAA,EAC1B,SAAS;AAAA,IACR,MAAM,SAAS;AACd,UAAI;AACH,cAAM,MAAM,MAAMA,gBAAI,kBAAkB,EAAE,IAAI,KAAK,IAAI,MAAM,GAAG,MAAM,GAAC,CAAG;AAC1E,aAAK,YAAY,MAAM,QAAQ,2BAAK,IAAI,IAAI,IAAI,OAAQ,MAAM,QAAQ,GAAG,IAAI,MAAM,CAAA;AAAA,eAC5E,GAAG;AAAEC,sBAAAA,MAAI,UAAU,EAAE,OAAO,QAAQ,MAAM,OAAO,CAAC;AAAA,MAAE;AAAA,IAC5D;AAAA,IACD,OAAO,GAAG;AACT,YAAM,SAAS,gBAAiB,EAAC,gBAAe,EAAG,SAAO,CAAC;AAC3D,UAAI,UAAU,OAAO,KAAK;AACzB,eAAO,IAAI,MAAM,aAAa,EAAE;AAChC,eAAO,IAAI,eAAe,EAAE;AAAA,MAC7B;AACAA,oBAAAA,MAAI,aAAa;AAAA,IAClB;AAAA,EACD;AACD;;;;;;;;;;;;;;;;;;ACnCD,GAAG,WAAW,eAAe;"}

View File

@@ -5,6 +5,11 @@ if (!Math) {
"./pages/index/index.js";
"./pages/order/create.js";
"./pages/product/select.js";
"./pages/product/list.js";
"./pages/product/form.js";
"./pages/product/categories.js";
"./pages/product/units.js";
"./pages/product/settings.js";
"./pages/customer/select.js";
"./pages/supplier/select.js";
"./pages/account/select.js";

View File

@@ -3,6 +3,11 @@
"pages/index/index",
"pages/order/create",
"pages/product/select",
"pages/product/list",
"pages/product/form",
"pages/product/categories",
"pages/product/units",
"pages/product/settings",
"pages/customer/select",
"pages/supplier/select",
"pages/account/select"

View File

@@ -2,18 +2,21 @@
const common_vendor = require("./vendor.js");
const envBaseUrl = typeof process !== "undefined" && process.env && (process.env.VITE_APP_API_BASE_URL || process.env.API_BASE_URL) || "";
const storageBaseUrl = typeof common_vendor.index !== "undefined" ? common_vendor.index.getStorageSync("API_BASE_URL") || "" : "";
const fallbackBaseUrl = "http://localhost:8080";
const fallbackBaseUrl = "http://192.168.31.193:8080";
const API_BASE_URL = (envBaseUrl || storageBaseUrl || fallbackBaseUrl).replace(/\/$/, "");
const candidateBases = [envBaseUrl, storageBaseUrl, fallbackBaseUrl, "http://127.0.0.1:8080", "http://localhost:8080"];
const API_BASE_URL_CANDIDATES = Array.from(new Set(candidateBases.filter(Boolean))).map((u) => String(u).replace(/\/$/, ""));
const envShopId = typeof process !== "undefined" && process.env && (process.env.VITE_APP_SHOP_ID || process.env.SHOP_ID) || "";
const storageShopId = typeof common_vendor.index !== "undefined" ? common_vendor.index.getStorageSync("SHOP_ID") || "" : "";
const SHOP_ID = Number(envShopId || storageShopId || 1);
const envEnableDefaultUser = typeof process !== "undefined" && process.env && (process.env.VITE_APP_ENABLE_DEFAULT_USER || process.env.ENABLE_DEFAULT_USER) || "";
const storageEnableDefaultUser = typeof common_vendor.index !== "undefined" ? common_vendor.index.getStorageSync("ENABLE_DEFAULT_USER") || "" : "";
const ENABLE_DEFAULT_USER = String(envEnableDefaultUser || storageEnableDefaultUser || "false").toLowerCase() === "true";
const ENABLE_DEFAULT_USER = String(envEnableDefaultUser || storageEnableDefaultUser || "true").toLowerCase() === "true";
const envDefaultUserId = typeof process !== "undefined" && process.env && (process.env.VITE_APP_DEFAULT_USER_ID || process.env.DEFAULT_USER_ID) || "";
const storageDefaultUserId = typeof common_vendor.index !== "undefined" ? common_vendor.index.getStorageSync("DEFAULT_USER_ID") || "" : "";
const DEFAULT_USER_ID = Number(envDefaultUserId || storageDefaultUserId || 2);
exports.API_BASE_URL = API_BASE_URL;
exports.API_BASE_URL_CANDIDATES = API_BASE_URL_CANDIDATES;
exports.DEFAULT_USER_ID = DEFAULT_USER_ID;
exports.ENABLE_DEFAULT_USER = ENABLE_DEFAULT_USER;
exports.SHOP_ID = SHOP_ID;

View File

@@ -8,24 +8,29 @@ function buildUrl(path) {
return path;
return common_config.API_BASE_URL + (path.startsWith("/") ? path : "/" + path);
}
function requestWithFallback(options, candidates, idx, resolve, reject) {
const base = candidates[idx] || common_config.API_BASE_URL;
const url = options.url.replace(/^https?:\/\/[^/]+/, base);
common_vendor.index.request({ ...options, url, success: (res) => {
const { statusCode, data } = res;
if (statusCode >= 200 && statusCode < 300)
return resolve(data);
if (idx + 1 < candidates.length)
return requestWithFallback(options, candidates, idx + 1, resolve, reject);
reject(new Error("HTTP " + statusCode));
}, fail: (err) => {
if (idx + 1 < candidates.length)
return requestWithFallback(options, candidates, idx + 1, resolve, reject);
reject(err);
} });
}
function get(path, params = {}) {
return new Promise((resolve, reject) => {
const headers = { "X-Shop-Id": common_config.SHOP_ID };
if (common_config.ENABLE_DEFAULT_USER && common_config.DEFAULT_USER_ID)
headers["X-User-Id"] = common_config.DEFAULT_USER_ID;
common_vendor.index.request({
url: buildUrl(path),
method: "GET",
data: params,
header: headers,
success: (res) => {
const { statusCode, data } = res;
if (statusCode >= 200 && statusCode < 300)
return resolve(data);
reject(new Error("HTTP " + statusCode));
},
fail: (err) => reject(err)
});
const options = { url: buildUrl(path), method: "GET", data: params, header: headers };
requestWithFallback(options, common_config.API_BASE_URL_CANDIDATES, 0, resolve, reject);
});
}
function post(path, body = {}) {
@@ -33,21 +38,67 @@ function post(path, body = {}) {
const headers = { "Content-Type": "application/json", "X-Shop-Id": common_config.SHOP_ID };
if (common_config.ENABLE_DEFAULT_USER && common_config.DEFAULT_USER_ID)
headers["X-User-Id"] = common_config.DEFAULT_USER_ID;
common_vendor.index.request({
url: buildUrl(path),
method: "POST",
data: body,
header: headers,
success: (res) => {
const { statusCode, data } = res;
if (statusCode >= 200 && statusCode < 300)
return resolve(data);
reject(new Error("HTTP " + statusCode));
},
fail: (err) => reject(err)
});
const options = { url: buildUrl(path), method: "POST", data: body, header: headers };
requestWithFallback(options, common_config.API_BASE_URL_CANDIDATES, 0, resolve, reject);
});
}
function put(path, body = {}) {
return new Promise((resolve, reject) => {
const headers = { "Content-Type": "application/json", "X-Shop-Id": common_config.SHOP_ID };
if (common_config.ENABLE_DEFAULT_USER && common_config.DEFAULT_USER_ID)
headers["X-User-Id"] = common_config.DEFAULT_USER_ID;
const options = { url: buildUrl(path), method: "PUT", data: body, header: headers };
requestWithFallback(options, common_config.API_BASE_URL_CANDIDATES, 0, resolve, reject);
});
}
function del(path, body = {}) {
return new Promise((resolve, reject) => {
const headers = { "Content-Type": "application/json", "X-Shop-Id": common_config.SHOP_ID };
if (common_config.ENABLE_DEFAULT_USER && common_config.DEFAULT_USER_ID)
headers["X-User-Id"] = common_config.DEFAULT_USER_ID;
const options = { url: buildUrl(path), method: "DELETE", data: body, header: headers };
requestWithFallback(options, common_config.API_BASE_URL_CANDIDATES, 0, resolve, reject);
});
}
function uploadWithFallback(options, candidates, idx, resolve, reject) {
const base = candidates[idx] || common_config.API_BASE_URL;
const url = options.url.replace(/^https?:\/\/[^/]+/, base);
const uploadOptions = { ...options, url };
common_vendor.index.uploadFile({
...uploadOptions,
success: (res) => {
const statusCode = res.statusCode || 0;
if (statusCode >= 200 && statusCode < 300) {
try {
const data = typeof res.data === "string" ? JSON.parse(res.data) : res.data;
return resolve(data);
} catch (e) {
return resolve(res.data);
}
}
if (idx + 1 < candidates.length)
return uploadWithFallback(options, candidates, idx + 1, resolve, reject);
reject(new Error("HTTP " + statusCode));
},
fail: (err) => {
if (idx + 1 < candidates.length)
return uploadWithFallback(options, candidates, idx + 1, resolve, reject);
reject(err);
}
});
}
function upload(path, filePath, formData = {}, name = "file") {
return new Promise((resolve, reject) => {
const header = { "X-Shop-Id": common_config.SHOP_ID };
if (common_config.ENABLE_DEFAULT_USER && common_config.DEFAULT_USER_ID)
header["X-User-Id"] = common_config.DEFAULT_USER_ID;
const options = { url: buildUrl(path), filePath, name, formData, header };
uploadWithFallback(options, common_config.API_BASE_URL_CANDIDATES, 0, resolve, reject);
});
}
exports.del = del;
exports.get = get;
exports.post = post;
exports.put = put;
exports.upload = upload;
//# sourceMappingURL=../../.sourcemap/mp-weixin/common/http.js.map

View File

@@ -68,8 +68,8 @@ const capitalize = cacheStringFunction((str) => {
return str.charAt(0).toUpperCase() + str.slice(1);
});
const toHandlerKey = cacheStringFunction((str) => {
const s = str ? `on${capitalize(str)}` : ``;
return s;
const s2 = str ? `on${capitalize(str)}` : ``;
return s2;
});
const hasChanged = (value, oldValue) => !Object.is(value, oldValue);
const invokeArrayFns$1 = (fns, arg) => {
@@ -92,6 +92,36 @@ const toNumber = (val) => {
const n = isString(val) ? Number(val) : NaN;
return isNaN(n) ? val : n;
};
function normalizeStyle(value) {
if (isArray(value)) {
const res = {};
for (let i = 0; i < value.length; i++) {
const item = value[i];
const normalized = isString(item) ? parseStringStyle(item) : normalizeStyle(item);
if (normalized) {
for (const key in normalized) {
res[key] = normalized[key];
}
}
}
return res;
} else if (isString(value) || isObject(value)) {
return value;
}
}
const listDelimiterRE = /;(?![^(]*\))/g;
const propertyDelimiterRE = /:([^]+)/;
const styleCommentRE = /\/\*[^]*?\*\//g;
function parseStringStyle(cssText) {
const ret = {};
cssText.replace(styleCommentRE, "").split(listDelimiterRE).forEach((item) => {
if (item) {
const tmp = item.split(propertyDelimiterRE);
tmp.length > 1 && (ret[tmp[0].trim()] = tmp[1].trim());
}
});
return ret;
}
const toDisplayString = (val) => {
return isString(val) ? val : val == null ? "" : isArray(val) || isObject(val) && (val.toString === objectToString || !isFunction(val.toString)) ? JSON.stringify(val, replacer, 2) : String(val);
};
@@ -1287,6 +1317,9 @@ function isReadonly(value) {
function isShallow(value) {
return !!(value && value["__v_isShallow"]);
}
function isProxy(value) {
return isReactive(value) || isReadonly(value);
}
function toRaw(observed) {
const raw = observed && observed["__v_raw"];
return raw ? toRaw(raw) : observed;
@@ -2078,6 +2111,47 @@ function setCurrentRenderingInstance(instance) {
instance && instance.type.__scopeId || null;
return prev;
}
const COMPONENTS = "components";
function resolveComponent(name, maybeSelfReference) {
return resolveAsset(COMPONENTS, name, true, maybeSelfReference) || name;
}
function resolveAsset(type, name, warnMissing = true, maybeSelfReference = false) {
const instance = currentRenderingInstance || currentInstance;
if (instance) {
const Component2 = instance.type;
if (type === COMPONENTS) {
const selfName = getComponentName(
Component2,
false
);
if (selfName && (selfName === name || selfName === camelize(name) || selfName === capitalize(camelize(name)))) {
return Component2;
}
}
const res = (
// local registration
// check instance[type] first which is resolved for options API
resolve(instance[type] || Component2[type], name) || // global registration
resolve(instance.appContext[type], name)
);
if (!res && maybeSelfReference) {
return Component2;
}
if (warnMissing && !res) {
const extra = type === COMPONENTS ? `
If this is a native custom element, make sure to exclude it from component resolution via compilerOptions.isCustomElement.` : ``;
warn$1(`Failed to resolve ${type.slice(0, -1)}: ${name}${extra}`);
}
return res;
} else {
warn$1(
`resolve${capitalize(type.slice(0, -1))} can only be used in render() or setup().`
);
}
}
function resolve(registry, name) {
return registry && (registry[name] || registry[camelize(name)] || registry[capitalize(camelize(name))]);
}
const INITIAL_WATCHER_VALUE = {};
function watch(source, cb, options) {
if (!isFunction(cb)) {
@@ -3692,6 +3766,12 @@ const Static = Symbol.for("v-stc");
function isVNode(value) {
return value ? value.__v_isVNode === true : false;
}
const InternalObjectKey = `__vInternal`;
function guardReactiveProps(props) {
if (!props)
return null;
return isProxy(props) || InternalObjectKey in props ? extend({}, props) : props;
}
const emptyAppContext = createAppContext();
let uid = 0;
function createComponentInstance(vnode, parent, suspense) {
@@ -4932,6 +5012,11 @@ function initApp(app) {
}
}
const propsCaches = /* @__PURE__ */ Object.create(null);
function renderProps(props) {
const { uid: uid2, __counter } = getCurrentInstance();
const propsId = (propsCaches[uid2] || (propsCaches[uid2] = [])).push(guardReactiveProps(props)) - 1;
return uid2 + "," + propsId + "," + __counter;
}
function pruneComponentPropsCache(uid2) {
delete propsCaches[uid2];
}
@@ -4972,6 +5057,22 @@ function getCreateApp() {
return my[method];
}
}
function stringifyStyle(value) {
if (isString(value)) {
return value;
}
return stringify(normalizeStyle(value));
}
function stringify(styles) {
let ret = "";
if (!styles || isString(styles)) {
return ret;
}
for (const key in styles) {
ret += `${key.startsWith(`--`) ? key : hyphenate(key)}:${styles[key]};`;
}
return ret;
}
function vOn(value, key) {
const instance = getCurrentInstance();
const ctx = instance.ctx;
@@ -5121,8 +5222,10 @@ function withModelModifiers(fn, { number, trim }, isComponent = false) {
}
const o = (value, key) => vOn(value, key);
const f = (source, renderItem) => vFor(source, renderItem);
const s = (value) => stringifyStyle(value);
const e = (target, ...sources) => extend(target, ...sources);
const t = (val) => toDisplayString(val);
const p = (props) => renderProps(props);
const m = (fn, modifiers, isComponent = false) => withModelModifiers(fn, modifiers, isComponent);
function createApp$1(rootComponent, rootProps = null) {
rootComponent && (rootComponent.mpType = "app");
@@ -5445,8 +5548,8 @@ function promisify$1(name, fn) {
if (hasCallback(args)) {
return wrapperReturnValue(name, invokeApi(name, fn, extend({}, args), rest));
}
return wrapperReturnValue(name, handlePromise(new Promise((resolve, reject) => {
invokeApi(name, fn, extend({}, args, { success: resolve, fail: reject }), rest);
return wrapperReturnValue(name, handlePromise(new Promise((resolve2, reject) => {
invokeApi(name, fn, extend({}, args, { success: resolve2, fail: reject }), rest);
})));
};
}
@@ -5767,7 +5870,7 @@ function invokeGetPushCidCallbacks(cid2, errMsg) {
getPushCidCallbacks.length = 0;
}
const API_GET_PUSH_CLIENT_ID = "getPushClientId";
const getPushClientId = defineAsyncApi(API_GET_PUSH_CLIENT_ID, (_, { resolve, reject }) => {
const getPushClientId = defineAsyncApi(API_GET_PUSH_CLIENT_ID, (_, { resolve: resolve2, reject }) => {
Promise.resolve().then(() => {
if (typeof enabled === "undefined") {
enabled = false;
@@ -5776,7 +5879,7 @@ const getPushClientId = defineAsyncApi(API_GET_PUSH_CLIENT_ID, (_, { resolve, re
}
getPushCidCallbacks.push((cid2, errMsg) => {
if (cid2) {
resolve({ cid: cid2 });
resolve2({ cid: cid2 });
} else {
reject(errMsg);
}
@@ -5845,9 +5948,9 @@ function promisify(name, api) {
if (isFunction(options.success) || isFunction(options.fail) || isFunction(options.complete)) {
return wrapperReturnValue(name, invokeApi(name, api, extend({}, options), rest));
}
return wrapperReturnValue(name, handlePromise(new Promise((resolve, reject) => {
return wrapperReturnValue(name, handlePromise(new Promise((resolve2, reject) => {
invokeApi(name, api, extend({}, options, {
success: resolve,
success: resolve2,
fail: reject
}), rest);
})));
@@ -6454,13 +6557,13 @@ function initRuntimeSocket(hosts, port, id) {
}
const SOCKET_TIMEOUT = 500;
function tryConnectSocket(host2, port, id) {
return new Promise((resolve, reject) => {
return new Promise((resolve2, reject) => {
const socket = index.connectSocket({
url: `ws://${host2}:${port}/${id}`,
multiple: true,
// 支付宝小程序 是否开启多实例
fail() {
resolve(null);
resolve2(null);
}
});
const timer = setTimeout(() => {
@@ -6468,19 +6571,19 @@ function tryConnectSocket(host2, port, id) {
code: 1006,
reason: "connect timeout"
});
resolve(null);
resolve2(null);
}, SOCKET_TIMEOUT);
socket.onOpen((e2) => {
clearTimeout(timer);
resolve(socket);
resolve2(socket);
});
socket.onClose((e2) => {
clearTimeout(timer);
resolve(null);
resolve2(null);
});
socket.onError((e2) => {
clearTimeout(timer);
resolve(null);
resolve2(null);
});
});
}
@@ -7898,5 +8001,8 @@ exports.f = f;
exports.index = index;
exports.m = m;
exports.o = o;
exports.p = p;
exports.resolveComponent = resolveComponent;
exports.s = s;
exports.t = t;
//# sourceMappingURL=../../.sourcemap/mp-weixin/common/vendor.js.map

View File

@@ -0,0 +1,144 @@
"use strict";
const common_vendor = require("../common/vendor.js");
const common_http = require("../common/http.js");
const ITEM_SIZE = 210;
const GAP = 18;
const COLS = 3;
function px(rpx) {
return rpx;
}
const _sfc_main = {
name: "ImageUploader",
props: {
modelValue: { type: Array, default: () => [] },
max: { type: Number, default: 9 },
uploadPath: { type: String, default: "/api/attachments" },
uploadFieldName: { type: String, default: "file" },
formData: { type: Object, default: () => ({ ownerType: "product" }) }
},
data() {
return {
innerList: []
};
},
computed: {
areaHeight() {
const rows = Math.ceil((this.innerList.length + 1) / COLS) || 1;
return rows * ITEM_SIZE + (rows - 1) * GAP;
}
},
watch: {
modelValue: {
immediate: true,
handler(list) {
const mapped = (list || []).map((u, i) => ({
uid: String(i) + "_" + (u.id || u.url || Math.random().toString(36).slice(2)),
url: typeof u === "string" ? u : u.url || "",
x: this.posOf(i).x,
y: this.posOf(i).y
}));
this.innerList = mapped;
}
}
},
methods: {
posOf(index) {
const row = Math.floor(index / COLS);
const col = index % COLS;
return { x: px(col * (ITEM_SIZE + GAP)), y: px(row * (ITEM_SIZE + GAP)) };
},
cellStyle(index) {
return {
width: ITEM_SIZE + "rpx",
height: ITEM_SIZE + "rpx"
};
},
preview(index) {
common_vendor.index.previewImage({ urls: this.innerList.map((i) => i.url), current: index });
},
remove(index) {
this.innerList.splice(index, 1);
this.reflow();
this.emit();
},
choose() {
const remain = this.max - this.innerList.length;
if (remain <= 0)
return;
common_vendor.index.chooseImage({ count: remain, success: async (res) => {
for (const path of res.tempFilePaths) {
await this.doUpload(path);
}
} });
},
async doUpload(filePath) {
var _a;
try {
const resp = await common_http.upload(this.uploadPath, filePath, this.formData, this.uploadFieldName);
const url = (resp == null ? void 0 : resp.url) || ((_a = resp == null ? void 0 : resp.data) == null ? void 0 : _a.url) || (resp == null ? void 0 : resp.path) || "";
if (!url)
throw new Error("上传响应无 url");
this.innerList.push({ uid: Math.random().toString(36).slice(2), url, ...this.posOf(this.innerList.length) });
this.reflow();
this.emit();
} catch (e) {
common_vendor.index.showToast({ title: "上传失败", icon: "none" });
}
},
onMoving(index, e) {
const { x, y } = e.detail;
this.innerList[index].x = x;
this.innerList[index].y = y;
},
onMoveEnd(index) {
const mv = this.innerList[index];
const col = Math.round(mv.x / (ITEM_SIZE + GAP));
const row = Math.round(mv.y / (ITEM_SIZE + GAP));
let newIndex = row * COLS + col;
newIndex = Math.max(0, Math.min(newIndex, this.innerList.length - 1));
if (newIndex !== index) {
const moved = this.innerList.splice(index, 1)[0];
this.innerList.splice(newIndex, 0, moved);
}
this.reflow();
this.emit();
},
reflow() {
this.innerList.forEach((it, i) => {
const p = this.posOf(i);
it.x = p.x;
it.y = p.y;
});
},
emit() {
this.$emit("update:modelValue", this.innerList.map((i) => i.url));
this.$emit("change", this.innerList.map((i) => i.url));
}
}
};
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
return common_vendor.e({
a: common_vendor.f($data.innerList, (img, index, i0) => {
return {
a: img.url,
b: common_vendor.o(($event) => $options.preview(index), img.uid),
c: common_vendor.o(($event) => $options.remove(index), img.uid),
d: img.uid,
e: common_vendor.s($options.cellStyle(index)),
f: img.x,
g: img.y,
h: common_vendor.o(($event) => $options.onMoving(index, $event), img.uid),
i: common_vendor.o(($event) => $options.onMoveEnd(index), img.uid)
};
}),
b: $data.innerList.length < $props.max
}, $data.innerList.length < $props.max ? {
c: common_vendor.o((...args) => $options.choose && $options.choose(...args))
} : {}, {
d: $options.areaHeight + "rpx",
e: $options.areaHeight + "rpx"
});
}
const Component = /* @__PURE__ */ common_vendor._export_sfc(_sfc_main, [["render", _sfc_render]]);
wx.createComponent(Component);
//# sourceMappingURL=../../.sourcemap/mp-weixin/components/ImageUploader.js.map

View File

@@ -0,0 +1,4 @@
{
"component": true,
"usingComponents": {}
}

View File

@@ -0,0 +1 @@
<view class="uploader"><view class="grid" style="{{'height:' + e}}"><movable-area class="area" style="{{'height:' + d}}"><movable-view wx:for="{{a}}" wx:for-item="img" wx:key="d" class="cell" style="{{img.e}}" direction="{{'all'}}" damping="{{40}}" friction="{{2}}" x="{{img.f}}" y="{{img.g}}" bindchange="{{img.h}}" bindtouchend="{{img.i}}"><image src="{{img.a}}" mode="aspectFill" class="thumb" bindtap="{{img.b}}"/><view class="remove" catchtap="{{img.c}}">×</view></movable-view><view wx:if="{{b}}" class="adder" bindtap="{{c}}"><text></text></view></movable-area></view></view>

View File

@@ -0,0 +1,15 @@
.uploader { padding: 12rpx; background: #fff;
}
.grid { position: relative;
}
.area { width: 100%; position: relative;
}
.cell { position: absolute; border-radius: 12rpx; overflow: hidden; box-shadow: 0 0 1rpx rgba(0,0,0,0.08);
}
.thumb { width: 100%; height: 100%;
}
.remove { position: absolute; right: 6rpx; top: 6rpx; background: rgba(0,0,0,0.45); color: #fff; width: 40rpx; height: 40rpx; text-align: center; line-height: 40rpx; border-radius: 20rpx; font-size: 28rpx;
}
.adder { width: 210rpx; height: 210rpx; border: 2rpx dashed #ccc; border-radius: 12rpx; display: flex; align-items: center; justify-content: center; color: #999; position: absolute; left: 0; top: 0;
}

View File

@@ -11,6 +11,7 @@ const _sfc_main = {
loadingNotices: false,
noticeError: "",
features: [
{ key: "product", title: "货品", img: "/static/icons/product.png", emoji: "📦" },
{ key: "customer", title: "客户", img: "/static/icons/customer.png", emoji: "👥" },
{ key: "sale", title: "销售", img: "/static/icons/sale.png", emoji: "💰" },
{ key: "account", title: "账户", img: "/static/icons/account.png", emoji: "💳" },
@@ -30,12 +31,14 @@ const _sfc_main = {
methods: {
async fetchMetrics() {
try {
const d = await common_http.get("/api/metrics/overview");
const d = await common_http.get("/api/dashboard/overview");
const toNum = (v) => typeof v === "number" ? v : Number(v || 0);
this.kpi = {
todaySales: d && d.todaySales || "0.00",
monthSales: d && d.monthSales || "0.00",
monthProfit: d && d.monthProfit || "0.00",
stockCount: d && d.stockCount || "0"
...this.kpi,
todaySales: toNum(d && d.todaySalesAmount).toFixed(2),
monthSales: toNum(d && d.monthSalesAmount).toFixed(2),
monthProfit: toNum(d && d.monthGrossProfit).toFixed(2),
stockCount: String((d && d.stockTotalQuantity) != null ? d.stockTotalQuantity : 0)
};
} catch (e) {
}
@@ -56,8 +59,16 @@ const _sfc_main = {
}
},
onFeatureTap(item) {
if (item.key === "product") {
common_vendor.index.navigateTo({ url: "/pages/product/list" });
return;
}
common_vendor.index.showToast({ title: item.title + "(开发中)", icon: "none" });
},
goProduct() {
this.activeTab = "product";
common_vendor.index.navigateTo({ url: "/pages/product/list" });
},
onCreateOrder() {
common_vendor.index.navigateTo({ url: "/pages/order/create" });
},
@@ -116,7 +127,7 @@ function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
l: $data.activeTab === "home" ? 1 : "",
m: common_vendor.o(($event) => $data.activeTab = "home"),
n: $data.activeTab === "product" ? 1 : "",
o: common_vendor.o(($event) => $data.activeTab = "product"),
o: common_vendor.o((...args) => $options.goProduct && $options.goProduct(...args)),
p: common_vendor.o((...args) => $options.onCreateOrder && $options.onCreateOrder(...args)),
q: $data.activeTab === "detail" ? 1 : "",
r: common_vendor.o(($event) => $data.activeTab = "detail"),

View File

@@ -0,0 +1,62 @@
"use strict";
const common_vendor = require("../../common/vendor.js");
const common_http = require("../../common/http.js");
const _sfc_main = {
data() {
return { name: "", list: [] };
},
onLoad() {
this.reload();
},
methods: {
async reload() {
try {
const res = await common_http.get("/api/product-categories");
this.list = Array.isArray(res == null ? void 0 : res.list) ? res.list : Array.isArray(res) ? res : [];
} catch (_) {
}
},
async create() {
if (!this.name)
return;
await common_http.post("/api/product-categories", { name: this.name });
this.name = "";
this.reload();
},
async update(c) {
await common_http.put("/api/product-categories/" + c.id, { name: c.name });
common_vendor.index.showToast({ title: "已保存", icon: "success" });
},
async remove(c) {
common_vendor.index.showModal({ content: "确定删除该类别?", success: async (r) => {
if (!r.confirm)
return;
await common_http.del("/api/product-categories/" + c.id);
this.reload();
} });
}
}
};
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
return {
a: $data.name,
b: common_vendor.o(common_vendor.m(($event) => $data.name = $event.detail.value, {
trim: true
})),
c: common_vendor.o((...args) => $options.create && $options.create(...args)),
d: common_vendor.f($data.list, (c, k0, i0) => {
return {
a: c.name,
b: common_vendor.o(common_vendor.m(($event) => c.name = $event.detail.value, {
trim: true
}), c.id),
c: common_vendor.o(($event) => $options.update(c), c.id),
d: common_vendor.o(($event) => $options.remove(c), c.id),
e: c.id
};
})
};
}
const MiniProgramPage = /* @__PURE__ */ common_vendor._export_sfc(_sfc_main, [["render", _sfc_render]]);
wx.createPage(MiniProgramPage);
//# sourceMappingURL=../../../.sourcemap/mp-weixin/pages/product/categories.js.map

View File

@@ -0,0 +1,4 @@
{
"navigationBarTitleText": "类别管理",
"usingComponents": {}
}

View File

@@ -0,0 +1 @@
<view class="page"><view class="toolbar"><input placeholder="新类别名称" value="{{a}}" bindinput="{{b}}"/><button size="mini" bindtap="{{c}}">新增</button></view><scroll-view scroll-y class="list"><view wx:for="{{d}}" wx:for-item="c" wx:key="e" class="item"><input value="{{c.a}}" bindinput="{{c.b}}"/><view class="ops"><button size="mini" bindtap="{{c.c}}">保存</button><button size="mini" type="warn" bindtap="{{c.d}}">删除</button></view></view></scroll-view></view>

View File

@@ -0,0 +1,15 @@
.page { display:flex; flex-direction: column; height: 100vh;
}
.toolbar { display:flex; gap: 12rpx; padding: 16rpx; background:#fff;
}
.toolbar input { flex:1; background:#f6f6f6; border-radius: 12rpx; padding: 12rpx;
}
.list { flex:1;
}
.item { display:flex; gap: 12rpx; align-items:center; padding: 16rpx; background:#fff; border-bottom: 1rpx solid #f1f1f1;
}
.item input { flex:1; background:#f7f7f7; border-radius: 10rpx; padding: 12rpx;
}
.ops { display:flex; gap: 10rpx;
}

View File

@@ -0,0 +1,251 @@
"use strict";
const common_vendor = require("../../common/vendor.js");
const common_http = require("../../common/http.js");
function parseSafeStock(text) {
const m = String(text || "").match(/\s*(\d+(?:\.\d+)?)\s*(?:-|~|—|到|,)?\s*(\d+(?:\.\d+)?)?\s*/);
if (!m)
return { min: null, max: null };
return { min: m[1] ? Number(m[1]) : null, max: m[2] ? Number(m[2]) : null };
}
const _sfc_main = {
data() {
return {
id: null,
units: [],
categories: [],
form: {
name: "",
categoryId: null,
unitId: null,
barcode: "",
brand: "",
model: "",
spec: "",
purchasePrice: 0,
wholesalePrice: 0,
bigClientPrice: 0,
retailPrice: 0,
stock: 0,
safeMin: null,
safeMax: null,
images: []
}
};
},
onLoad(query) {
this.id = query && query.id ? Number(query.id) : null;
this.bootstrap();
},
computed: {
categoryLabel() {
const c = this.categories.find((x) => x.id === this.form.categoryId);
return c ? c.name : "未选择";
},
unitLabel() {
const u = this.units.find((x) => x.id === this.form.unitId);
return u ? u.name : "未选择";
},
safeStockText: {
get() {
const a = this.form.safeMin != null ? String(this.form.safeMin) : "";
const b = this.form.safeMax != null ? String(this.form.safeMax) : "";
if (!a && !b)
return "";
if (a && b)
return a + "-" + b;
return a || b;
},
set(v) {
const { min, max } = parseSafeStock(v);
this.form.safeMin = min;
this.form.safeMax = max;
}
}
},
methods: {
async bootstrap() {
try {
const [units, cats] = await Promise.all([
common_http.get("/api/product/units"),
common_http.get("/api/product/categories")
]);
this.units = Array.isArray(units == null ? void 0 : units.list) ? units.list : Array.isArray(units) ? units : [];
this.categories = Array.isArray(cats == null ? void 0 : cats.list) ? cats.list : Array.isArray(cats) ? cats : [];
} catch (_) {
}
if (this.id) {
try {
const d = await common_http.get("/api/product/detail", { id: this.id });
Object.assign(this.form, {
name: d.name || "",
categoryId: d.categoryId || null,
unitId: d.unitId || null,
barcode: d.barcode || "",
brand: d.brand || "",
model: d.model || "",
spec: d.spec || "",
purchasePrice: Number(d.purchasePrice || 0),
wholesalePrice: Number(d.wholesalePrice || 0),
bigClientPrice: Number(d.bigClientPrice || 0),
retailPrice: Number(d.retailPrice || 0),
stock: Number(d.stock || 0),
safeMin: d.safeMin ?? null,
safeMax: d.safeMax ?? null,
images: Array.isArray(d.images) ? d.images : []
});
} catch (_) {
}
}
},
chooseCategory() {
if (!this.categories.length)
return;
common_vendor.index.showActionSheet({ itemList: this.categories.map((c) => c.name), success: ({ tapIndex }) => {
this.form.categoryId = this.categories[tapIndex].id;
} });
},
chooseUnit() {
if (!this.units.length)
return;
common_vendor.index.showActionSheet({ itemList: this.units.map((u) => u.name), success: ({ tapIndex }) => {
this.form.unitId = this.units[tapIndex].id;
} });
},
scanBarcode() {
common_vendor.index.scanCode({ onlyFromCamera: false, success: (res) => {
this.form.barcode = res.result;
}, fail: () => {
common_vendor.index.showToast({ title: "扫码失败,请手动录入", icon: "none" });
} });
},
async chooseImages() {
common_vendor.index.chooseImage({ count: 6, sizeType: ["compressed"], success: async (res) => {
try {
const uploaded = [];
for (const path of res.tempFilePaths) {
const ret = await common_http.upload("/api/upload", path, { biz: "product" });
uploaded.push(ret.url || ret.path || path);
}
this.form.images = (this.form.images || []).concat(uploaded).slice(0, 9);
} catch (e) {
common_vendor.index.showToast({ title: "上传失败", icon: "none" });
}
} });
},
moveLeft(i) {
if (i <= 0)
return;
const a = this.form.images;
[a[i - 1], a[i]] = [a[i], a[i - 1]];
},
moveRight(i) {
const a = this.form.images;
if (i >= a.length - 1)
return;
[a[i + 1], a[i]] = [a[i], a[i + 1]];
},
removeImg(i) {
this.form.images.splice(i, 1);
},
async submit() {
if (!this.form.name) {
common_vendor.index.showToast({ title: "请填写名称", icon: "none" });
return;
}
const payload = {
name: this.form.name,
categoryId: this.form.categoryId,
unitId: this.form.unitId,
barcode: this.form.barcode,
brand: this.form.brand,
model: this.form.model,
spec: this.form.spec,
purchasePrice: Number(this.form.purchasePrice || 0),
wholesalePrice: Number(this.form.wholesalePrice || 0),
bigClientPrice: Number(this.form.bigClientPrice || 0),
retailPrice: Number(this.form.retailPrice || 0),
stock: Number(this.form.stock || 0),
safeMin: this.form.safeMin,
safeMax: this.form.safeMax,
images: this.form.images
};
try {
if (this.id)
await common_http.put("/api/product", { id: this.id, ...payload });
else
await common_http.post("/api/product", payload);
common_vendor.index.showToast({ title: "已保存", icon: "success" });
setTimeout(() => {
common_vendor.index.navigateBack();
}, 600);
} catch (e) {
common_vendor.index.showToast({ title: e && e.message || "保存失败", icon: "none" });
}
}
}
};
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
return {
a: $data.form.name,
b: common_vendor.o(common_vendor.m(($event) => $data.form.name = $event.detail.value, {
trim: true
})),
c: common_vendor.t($options.categoryLabel),
d: common_vendor.o((...args) => $options.chooseCategory && $options.chooseCategory(...args)),
e: common_vendor.t($options.unitLabel),
f: common_vendor.o((...args) => $options.chooseUnit && $options.chooseUnit(...args)),
g: $data.form.barcode,
h: common_vendor.o(common_vendor.m(($event) => $data.form.barcode = $event.detail.value, {
trim: true
})),
i: common_vendor.o((...args) => $options.scanBarcode && $options.scanBarcode(...args)),
j: $data.form.brand,
k: common_vendor.o(common_vendor.m(($event) => $data.form.brand = $event.detail.value, {
trim: true
})),
l: $data.form.model,
m: common_vendor.o(common_vendor.m(($event) => $data.form.model = $event.detail.value, {
trim: true
})),
n: $data.form.spec,
o: common_vendor.o(common_vendor.m(($event) => $data.form.spec = $event.detail.value, {
trim: true
})),
p: $data.form.purchasePrice,
q: common_vendor.o(common_vendor.m(($event) => $data.form.purchasePrice = $event.detail.value, {
number: true
})),
r: $data.form.wholesalePrice,
s: common_vendor.o(common_vendor.m(($event) => $data.form.wholesalePrice = $event.detail.value, {
number: true
})),
t: $data.form.bigClientPrice,
v: common_vendor.o(common_vendor.m(($event) => $data.form.bigClientPrice = $event.detail.value, {
number: true
})),
w: $data.form.retailPrice,
x: common_vendor.o(common_vendor.m(($event) => $data.form.retailPrice = $event.detail.value, {
number: true
})),
y: $data.form.stock,
z: common_vendor.o(common_vendor.m(($event) => $data.form.stock = $event.detail.value, {
number: true
})),
A: $options.safeStockText,
B: common_vendor.o(($event) => $options.safeStockText = $event.detail.value),
C: common_vendor.o((...args) => $options.chooseImages && $options.chooseImages(...args)),
D: common_vendor.f($data.form.images, (url, idx, i0) => {
return {
a: url,
b: common_vendor.o(($event) => $options.moveLeft(idx), idx),
c: common_vendor.o(($event) => $options.moveRight(idx), idx),
d: common_vendor.o(($event) => $options.removeImg(idx), idx),
e: idx
};
}),
E: common_vendor.o((...args) => $options.submit && $options.submit(...args))
};
}
const MiniProgramPage = /* @__PURE__ */ common_vendor._export_sfc(_sfc_main, [["render", _sfc_render]]);
wx.createPage(MiniProgramPage);
//# sourceMappingURL=../../../.sourcemap/mp-weixin/pages/product/edit.js.map

View File

@@ -0,0 +1,4 @@
{
"navigationBarTitleText": "编辑货品",
"usingComponents": {}
}

View File

@@ -0,0 +1 @@
<view class="page"><view class="form"><view class="field"><text class="label">名称</text><input class="value" placeholder="必填" value="{{a}}" bindinput="{{b}}"/></view><view class="field" bindtap="{{d}}"><text class="label">类别</text><text class="value">{{c}}</text></view><view class="field" bindtap="{{f}}"><text class="label">主单位</text><text class="value">{{e}}</text></view><view class="field"><text class="label">条形码</text><input class="value" placeholder="手动录入或扫码" value="{{g}}" bindinput="{{h}}"/><button size="mini" class="scan" bindtap="{{i}}">扫码</button></view><view class="field"><text class="label">品牌</text><input class="value" placeholder="可选" value="{{j}}" bindinput="{{k}}"/></view><view class="field"><text class="label">型号</text><input class="value" placeholder="可选" value="{{l}}" bindinput="{{m}}"/></view><view class="field"><text class="label">规格</text><input class="value" placeholder="可选" value="{{n}}" bindinput="{{o}}"/></view><view class="grid2"><view class="field"><text class="label">进货价</text><input class="value" type="digit" value="{{p}}" bindinput="{{q}}"/></view><view class="field"><text class="label">批发价</text><input class="value" type="digit" value="{{r}}" bindinput="{{s}}"/></view><view class="field"><text class="label">大单报价</text><input class="value" type="digit" value="{{t}}" bindinput="{{v}}"/></view><view class="field"><text class="label">零售价</text><input class="value" type="digit" value="{{w}}" bindinput="{{x}}"/></view></view><view class="grid2"><view class="field"><text class="label">当前库存</text><input class="value" type="digit" value="{{y}}" bindinput="{{z}}"/></view><view class="field"><text class="label">安全库存</text><input class="value" type="text" placeholder="如 10-100" value="{{A}}" bindinput="{{B}}"/></view></view><view class="photos"><view class="photos-header"><text>商品图片</text><button size="mini" bindtap="{{C}}">选择图片</button></view><view class="photo-list"><view wx:for="{{D}}" wx:for-item="url" wx:key="e" class="photo"><image src="{{url.a}}" mode="aspectFill"/><view class="photo-actions"><text class="btn" bindtap="{{url.b}}">←</text><text class="btn" bindtap="{{url.c}}">→</text><text class="btn danger" bindtap="{{url.d}}">删</text></view></view></view></view></view><view class="bottom"><button class="primary" bindtap="{{E}}">保存</button></view></view>

View File

@@ -0,0 +1,35 @@
.page{ padding-bottom: 140rpx;
}
.form{ background:#fff;
}
.field{ display:flex; align-items:center; gap: 12rpx; padding: 22rpx 20rpx; border-bottom: 1rpx solid #f2f2f2;
}
.label{ color:#666; width: 160rpx;
}
.value{ flex:1; color:#333;
}
.scan{ flex: 0 0 auto;
}
.grid2{ display:grid; grid-template-columns: 1fr 1fr;
}
.photos{ padding: 16rpx 20rpx;
}
.photos-header{ display:flex; justify-content: space-between; align-items:center; margin-bottom: 12rpx;
}
.photo-list{ display:grid; grid-template-columns: repeat(3, 1fr); gap: 12rpx;
}
.photo{ position: relative;
}
.photo image{ width: 100%; height: 200rpx; border-radius: 12rpx; background:#f6f6f6; display:block;
}
.photo-actions{ position:absolute; right:6rpx; bottom:6rpx; display:flex; gap: 6rpx;
}
.btn{ font-size: 22rpx; padding: 4rpx 8rpx; background: rgba(0,0,0,0.45); color:#fff; border-radius: 8rpx;
}
.btn.danger{ background: rgba(221,82,77,0.85);
}
.bottom{ position: fixed; left:0; right:0; bottom:0; background:#fff; padding: 16rpx 24rpx calc(env(safe-area-inset-bottom) + 16rpx); box-shadow: 0 -4rpx 12rpx rgba(0,0,0,0.06);
}
.primary{ width: 100%; background: linear-gradient(135deg, #FFE69A 0%, #F4CF62 45%, #D7A72E 100%); color:#493c1b; border-radius: 999rpx; padding: 20rpx 0; font-weight:800;
}

View File

@@ -0,0 +1,249 @@
"use strict";
const common_vendor = require("../../common/vendor.js");
const common_http = require("../../common/http.js");
const ImageUploader = () => "../../components/ImageUploader.js";
const _sfc_main = {
components: { ImageUploader },
data() {
return {
id: "",
form: {
name: "",
barcode: "",
brand: "",
model: "",
spec: "",
origin: "",
categoryId: "",
unitId: "",
stock: null,
safeMin: null,
safeMax: null,
purchasePrice: null,
retailPrice: null,
wholesalePrice: null,
bigClientPrice: null,
images: [],
remark: ""
},
units: [],
categories: []
};
},
onLoad(query) {
this.id = (query == null ? void 0 : query.id) || "";
this.bootstrap();
},
computed: {
unitNames() {
return this.units.map((u) => u.name);
},
categoryNames() {
return this.categories.map((c) => c.name);
},
unitLabel() {
const u = this.units.find((x) => String(x.id) === String(this.form.unitId));
return u ? u.name : "选择单位";
},
categoryLabel() {
const c = this.categories.find((x) => String(x.id) === String(this.form.categoryId));
return c ? c.name : "选择类别";
}
},
methods: {
async bootstrap() {
await Promise.all([this.fetchUnits(), this.fetchCategories()]);
if (this.id)
this.loadDetail();
},
async fetchUnits() {
try {
const res = await common_http.get("/api/product-units");
this.units = Array.isArray(res == null ? void 0 : res.list) ? res.list : Array.isArray(res) ? res : [];
} catch (_) {
}
},
async fetchCategories() {
try {
const res = await common_http.get("/api/product-categories");
this.categories = Array.isArray(res == null ? void 0 : res.list) ? res.list : Array.isArray(res) ? res : [];
} catch (_) {
}
},
onPickUnit(e) {
const idx = Number(e.detail.value);
const u = this.units[idx];
this.form.unitId = u ? u.id : "";
},
onPickCategory(e) {
const idx = Number(e.detail.value);
const c = this.categories[idx];
this.form.categoryId = c ? c.id : "";
},
scan() {
common_vendor.index.scanCode({ onlyFromCamera: false, success: (res) => {
this.form.barcode = res.result;
} });
},
async loadDetail() {
try {
const data = await common_http.get("/api/products/" + this.id);
Object.assign(this.form, {
name: data.name,
barcode: data.barcode,
brand: data.brand,
model: data.model,
spec: data.spec,
origin: data.origin,
categoryId: data.categoryId,
unitId: data.unitId,
stock: data.stock,
safeMin: data.safeMin,
safeMax: data.safeMax,
purchasePrice: data.purchasePrice,
retailPrice: data.retailPrice,
wholesalePrice: data.wholesalePrice,
bigClientPrice: data.bigClientPrice,
images: (data.images || []).map((i) => i.url || i)
});
} catch (_) {
}
},
validate() {
if (!this.form.name) {
common_vendor.index.showToast({ title: "请填写名称", icon: "none" });
return false;
}
if (this.form.safeMin != null && this.form.safeMax != null && Number(this.form.safeMin) > Number(this.form.safeMax)) {
common_vendor.index.showToast({ title: "安全库存区间不合法", icon: "none" });
return false;
}
return true;
},
buildPayload() {
const f = this.form;
return {
name: f.name,
barcode: f.barcode,
brand: f.brand,
model: f.model,
spec: f.spec,
origin: f.origin,
categoryId: f.categoryId || null,
unitId: f.unitId,
safeMin: f.safeMin,
safeMax: f.safeMax,
prices: {
purchasePrice: f.purchasePrice,
retailPrice: f.retailPrice,
wholesalePrice: f.wholesalePrice,
bigClientPrice: f.bigClientPrice
},
stock: f.stock,
images: f.images,
remark: f.remark
};
},
async save(goOn) {
if (!this.validate())
return;
const payload = this.buildPayload();
try {
if (this.id)
await common_http.put("/api/products/" + this.id, payload);
else
await common_http.post("/api/products", payload);
common_vendor.index.showToast({ title: "保存成功", icon: "success" });
if (goOn && !this.id) {
this.form = { name: "", barcode: "", brand: "", model: "", spec: "", origin: "", categoryId: "", unitId: "", stock: null, safeMin: null, safeMax: null, purchasePrice: null, retailPrice: null, wholesalePrice: null, bigClientPrice: null, images: [], remark: "" };
} else {
setTimeout(() => common_vendor.index.navigateBack(), 400);
}
} catch (e) {
common_vendor.index.showToast({ title: "保存失败", icon: "none" });
}
}
}
};
if (!Array) {
const _component_ImageUploader = common_vendor.resolveComponent("ImageUploader");
_component_ImageUploader();
}
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
return {
a: $data.form.name,
b: common_vendor.o(common_vendor.m(($event) => $data.form.name = $event.detail.value, {
trim: true
})),
c: $data.form.barcode,
d: common_vendor.o(common_vendor.m(($event) => $data.form.barcode = $event.detail.value, {
trim: true
})),
e: $data.form.brand,
f: common_vendor.o(common_vendor.m(($event) => $data.form.brand = $event.detail.value, {
trim: true
})),
g: $data.form.model,
h: common_vendor.o(common_vendor.m(($event) => $data.form.model = $event.detail.value, {
trim: true
})),
i: $data.form.spec,
j: common_vendor.o(common_vendor.m(($event) => $data.form.spec = $event.detail.value, {
trim: true
})),
k: $data.form.origin,
l: common_vendor.o(common_vendor.m(($event) => $data.form.origin = $event.detail.value, {
trim: true
})),
m: common_vendor.t($options.unitLabel),
n: $options.unitNames,
o: common_vendor.o((...args) => $options.onPickUnit && $options.onPickUnit(...args)),
p: common_vendor.t($options.categoryLabel),
q: $options.categoryNames,
r: common_vendor.o((...args) => $options.onPickCategory && $options.onPickCategory(...args)),
s: $data.form.stock,
t: common_vendor.o(common_vendor.m(($event) => $data.form.stock = $event.detail.value, {
number: true
})),
v: $data.form.safeMin,
w: common_vendor.o(common_vendor.m(($event) => $data.form.safeMin = $event.detail.value, {
number: true
})),
x: $data.form.safeMax,
y: common_vendor.o(common_vendor.m(($event) => $data.form.safeMax = $event.detail.value, {
number: true
})),
z: $data.form.purchasePrice,
A: common_vendor.o(common_vendor.m(($event) => $data.form.purchasePrice = $event.detail.value, {
number: true
})),
B: $data.form.retailPrice,
C: common_vendor.o(common_vendor.m(($event) => $data.form.retailPrice = $event.detail.value, {
number: true
})),
D: $data.form.wholesalePrice,
E: common_vendor.o(common_vendor.m(($event) => $data.form.wholesalePrice = $event.detail.value, {
number: true
})),
F: $data.form.bigClientPrice,
G: common_vendor.o(common_vendor.m(($event) => $data.form.bigClientPrice = $event.detail.value, {
number: true
})),
H: common_vendor.o(($event) => $data.form.images = $event),
I: common_vendor.p({
formData: {
ownerType: "product"
},
modelValue: $data.form.images
}),
J: $data.form.remark,
K: common_vendor.o(common_vendor.m(($event) => $data.form.remark = $event.detail.value, {
trim: true
})),
L: common_vendor.o(($event) => $options.save(false)),
M: common_vendor.o(($event) => $options.save(true))
};
}
const MiniProgramPage = /* @__PURE__ */ common_vendor._export_sfc(_sfc_main, [["render", _sfc_render]]);
wx.createPage(MiniProgramPage);
//# sourceMappingURL=../../../.sourcemap/mp-weixin/pages/product/form.js.map

View File

@@ -0,0 +1,6 @@
{
"navigationBarTitleText": "编辑货品",
"usingComponents": {
"image-uploader": "../../components/ImageUploader"
}
}

View File

@@ -0,0 +1 @@
<scroll-view scroll-y class="page"><view class="card"><view class="row"><text class="label">商品名称</text><input placeholder="必填" value="{{a}}" bindinput="{{b}}"/></view><view class="row"><text class="label">条形码</text><input placeholder="可扫码或输入" value="{{c}}" bindinput="{{d}}"/></view><view class="row"><text class="label">品牌/型号/规格/产地</text></view><view class="row"><input placeholder="品牌" value="{{e}}" bindinput="{{f}}"/></view><view class="row"><input placeholder="型号" value="{{g}}" bindinput="{{h}}"/></view><view class="row"><input placeholder="规格" value="{{i}}" bindinput="{{j}}"/></view><view class="row"><input placeholder="产地" value="{{k}}" bindinput="{{l}}"/></view><view class="row"><picker mode="selector" range="{{n}}" bindchange="{{o}}"><view class="picker">主单位:{{m}}</view></picker><picker mode="selector" range="{{q}}" bindchange="{{r}}"><view class="picker">类别:{{p}}</view></picker></view></view><view class="card"><view class="row"><text class="label">库存与安全库存</text></view><view class="row"><input type="number" placeholder="当前库存" value="{{s}}" bindinput="{{t}}"/><input type="number" placeholder="安全库存下限" value="{{v}}" bindinput="{{w}}"/><input type="number" placeholder="安全库存上限" value="{{x}}" bindinput="{{y}}"/></view></view><view class="card"><view class="row"><text class="label">价格(进价/零售/批发/大单)</text></view><view class="row prices"><input type="number" placeholder="进货价" value="{{z}}" bindinput="{{A}}"/><input type="number" placeholder="零售价" value="{{B}}" bindinput="{{C}}"/><input type="number" placeholder="批发价" value="{{D}}" bindinput="{{E}}"/><input type="number" placeholder="大单价" value="{{F}}" bindinput="{{G}}"/></view></view><view class="card"><text class="label">图片</text><image-uploader wx:if="{{I}}" u-i="4a3f460a-0" bind:__l="__l" bindupdateModelValue="{{H}}" u-p="{{I}}"/></view><view class="card"><text class="label">备注</text><block wx:if="{{r0}}"><textarea placeholder="可选" auto-height value="{{J}}" bindinput="{{K}}"/></block></view><view class="fixed"><button type="default" bindtap="{{L}}">保存</button><button type="primary" bindtap="{{M}}">保存并继续</button></view></scroll-view>

View File

@@ -0,0 +1,17 @@
.page { background:#f6f6f6; height: 100vh;
}
.card { background:#fff; margin: 16rpx; padding: 16rpx; border-radius: 12rpx;
}
.row { display:flex; gap: 12rpx; align-items: center; margin-bottom: 12rpx;
}
.label { width: 180rpx; color:#666;
}
.row input { flex:1; background:#f7f7f7; border-radius: 10rpx; padding: 12rpx;
}
.picker { padding: 8rpx 12rpx; background:#f0f0f0; border-radius: 10rpx; color:#666; margin-left: 8rpx;
}
.prices input { width: 30%;
}
.fixed { position: fixed; left: 0; right: 0; bottom: 0; background:#fff; padding: 12rpx 16rpx; display:flex; gap: 16rpx;
}

View File

@@ -0,0 +1,122 @@
"use strict";
const common_vendor = require("../../common/vendor.js");
const common_http = require("../../common/http.js");
const _sfc_main = {
data() {
return {
items: [],
query: { kw: "", page: 1, size: 20, categoryId: "" },
finished: false,
loading: false,
tab: "all",
categories: []
};
},
onLoad() {
this.fetchCategories();
this.reload();
},
computed: {
categoryNames() {
return this.categories.map((c) => c.name);
},
categoryLabel() {
const c = this.categories.find((x) => String(x.id) === String(this.query.categoryId));
return c ? "类别:" + c.name : "选择类别";
}
},
methods: {
switchTab(t) {
this.tab = t;
this.query.categoryId = "";
this.reload();
},
onPickCategory(e) {
const idx = Number(e.detail.value);
const c = this.categories[idx];
this.query.categoryId = c ? c.id : "";
this.reload();
},
async fetchCategories() {
try {
const res = await common_http.get("/api/product-categories", {});
this.categories = Array.isArray(res == null ? void 0 : res.list) ? res.list : Array.isArray(res) ? res : [];
} catch (_) {
}
},
reload() {
this.items = [];
this.query.page = 1;
this.finished = false;
this.loadMore();
},
async loadMore() {
if (this.loading || this.finished)
return;
this.loading = true;
try {
const params = { kw: this.query.kw, page: this.query.page, size: this.query.size };
if (this.tab === "category" && this.query.categoryId)
params.categoryId = this.query.categoryId;
const res = await common_http.get("/api/products", params);
const list = Array.isArray(res == null ? void 0 : res.list) ? res.list : Array.isArray(res) ? res : [];
this.items = this.items.concat(list);
if (list.length < this.query.size)
this.finished = true;
this.query.page += 1;
} catch (e) {
common_vendor.index.showToast({ title: "加载失败", icon: "none" });
} finally {
this.loading = false;
}
},
openForm(id) {
const url = "/pages/product/form" + (id ? "?id=" + id : "");
common_vendor.index.navigateTo({ url });
}
}
};
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
return common_vendor.e({
a: $data.tab === "all" ? 1 : "",
b: common_vendor.o(($event) => $options.switchTab("all")),
c: $data.tab === "category" ? 1 : "",
d: common_vendor.o(($event) => $options.switchTab("category")),
e: common_vendor.o((...args) => $options.reload && $options.reload(...args)),
f: $data.query.kw,
g: common_vendor.o(common_vendor.m(($event) => $data.query.kw = $event.detail.value, {
trim: true
})),
h: $data.tab === "category"
}, $data.tab === "category" ? {
i: common_vendor.t($options.categoryLabel),
j: $options.categoryNames,
k: common_vendor.o((...args) => $options.onPickCategory && $options.onPickCategory(...args))
} : {}, {
l: common_vendor.o((...args) => $options.reload && $options.reload(...args)),
m: $data.items.length
}, $data.items.length ? {
n: common_vendor.f($data.items, (it, k0, i0) => {
return common_vendor.e({
a: it.cover
}, it.cover ? {
b: it.cover
} : {}, {
c: common_vendor.t(it.name),
d: common_vendor.t(it.brand || "-"),
e: common_vendor.t(it.model || ""),
f: common_vendor.t(it.spec || ""),
g: common_vendor.t(it.stock ?? 0),
h: common_vendor.t((it.retailPrice ?? it.price ?? 0).toFixed(2)),
i: it.id,
j: common_vendor.o(($event) => $options.openForm(it.id), it.id)
});
})
} : {}, {
o: common_vendor.o((...args) => $options.loadMore && $options.loadMore(...args)),
p: common_vendor.o(($event) => $options.openForm())
});
}
const MiniProgramPage = /* @__PURE__ */ common_vendor._export_sfc(_sfc_main, [["render", _sfc_render]]);
wx.createPage(MiniProgramPage);
//# sourceMappingURL=../../../.sourcemap/mp-weixin/pages/product/list.js.map

View File

@@ -0,0 +1,4 @@
{
"navigationBarTitleText": "货品列表",
"usingComponents": {}
}

View File

@@ -0,0 +1 @@
<view class="page"><view class="tabs"><view class="{{['tab', a && 'active']}}" bindtap="{{b}}">全部</view><view class="{{['tab', c && 'active']}}" bindtap="{{d}}">按类别</view></view><view class="search"><input placeholder="输入名称/条码/规格查询" bindconfirm="{{e}}" value="{{f}}" bindinput="{{g}}"/><picker wx:if="{{h}}" mode="selector" range="{{j}}" bindchange="{{k}}"><view class="picker">{{i}}</view></picker><button size="mini" bindtap="{{l}}">查询</button></view><scroll-view scroll-y class="list" bindscrolltolower="{{o}}"><block wx:if="{{m}}"><view wx:for="{{n}}" wx:for-item="it" wx:key="i" class="item" bindtap="{{it.j}}"><image wx:if="{{it.a}}" src="{{it.b}}" class="thumb" mode="aspectFill"/><view class="content"><view class="name">{{it.c}}</view><view class="meta">{{it.d}} {{it.e}} {{it.f}}</view><view class="meta">库存:{{it.g}} <text class="price">零售价:¥{{it.h}}</text></view></view></view></block><view wx:else class="empty"><text>暂无数据,点击右上角“+”新增</text></view></scroll-view><view class="fab" bindtap="{{p}}"></view></view>

View File

@@ -0,0 +1,33 @@
.page { display:flex; flex-direction: column; height: 100vh;
}
.tabs { display:flex; background:#fff;
}
.tab { flex:1; text-align:center; padding: 20rpx 0; color:#666;
}
.tab.active { color:#18b566; font-weight: 600;
}
.search { display:flex; gap: 12rpx; padding: 16rpx; background:#fff; align-items: center;
}
.search input { flex:1; background:#f6f6f6; border-radius: 12rpx; padding: 12rpx;
}
.picker { padding: 8rpx 12rpx; background:#f0f0f0; border-radius: 10rpx; color:#666;
}
.list { flex:1;
}
.item { display:flex; padding: 20rpx; background:#fff; border-bottom: 1rpx solid #f1f1f1;
}
.thumb { width: 120rpx; height: 120rpx; border-radius: 12rpx; margin-right: 16rpx; background:#fafafa;
}
.content { flex:1;
}
.name { color:#333; margin-bottom: 6rpx; font-weight: 600;
}
.meta { color:#888; font-size: 24rpx;
}
.price { margin-left: 20rpx; color:#f60;
}
.empty { height: 60vh; display:flex; align-items:center; justify-content:center; color:#999;
}
.fab { position: fixed; right: 30rpx; bottom: 120rpx; width: 100rpx; height: 100rpx; background:#18b566; color:#fff; border-radius: 50rpx; text-align:center; line-height: 100rpx; font-size: 48rpx; box-shadow: 0 8rpx 20rpx rgba(0,0,0,0.15);
}

View File

@@ -0,0 +1,39 @@
"use strict";
const common_http = require("../../common/http.js");
const common_vendor = require("../../common/vendor.js");
const _sfc_main = {
data() {
return { settings: { hideZeroStock: false, hidePurchasePrice: false } };
},
onLoad() {
this.load();
},
methods: {
async load() {
try {
const res = await common_http.get("/api/product-settings");
this.settings = { hideZeroStock: !!(res == null ? void 0 : res.hideZeroStock), hidePurchasePrice: !!(res == null ? void 0 : res.hidePurchasePrice) };
} catch (_) {
}
},
async update(key, val) {
const next = { ...this.settings, [key]: val };
this.settings = next;
try {
await common_http.put("/api/product-settings", next);
} catch (_) {
}
}
}
};
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
return {
a: $data.settings.hideZeroStock,
b: common_vendor.o((e) => $options.update("hideZeroStock", e.detail.value)),
c: $data.settings.hidePurchasePrice,
d: common_vendor.o((e) => $options.update("hidePurchasePrice", e.detail.value))
};
}
const MiniProgramPage = /* @__PURE__ */ common_vendor._export_sfc(_sfc_main, [["render", _sfc_render]]);
wx.createPage(MiniProgramPage);
//# sourceMappingURL=../../../.sourcemap/mp-weixin/pages/product/settings.js.map

View File

@@ -0,0 +1,4 @@
{
"navigationBarTitleText": "货品设置",
"usingComponents": {}
}

View File

@@ -0,0 +1 @@
<view class="page"><view class="item"><text>隐藏零库存商品</text><switch checked="{{a}}" bindchange="{{b}}"/></view><view class="item"><text>隐藏进货价</text><switch checked="{{c}}" bindchange="{{d}}"/></view></view>

View File

@@ -0,0 +1,5 @@
.page { background:#fff;
}
.item { display:flex; justify-content: space-between; align-items:center; padding: 20rpx; border-bottom: 1rpx solid #f1f1f1;
}

View File

@@ -0,0 +1,62 @@
"use strict";
const common_vendor = require("../../common/vendor.js");
const common_http = require("../../common/http.js");
const _sfc_main = {
data() {
return { name: "", list: [] };
},
onLoad() {
this.reload();
},
methods: {
async reload() {
try {
const res = await common_http.get("/api/product-units");
this.list = Array.isArray(res == null ? void 0 : res.list) ? res.list : Array.isArray(res) ? res : [];
} catch (_) {
}
},
async create() {
if (!this.name)
return;
await common_http.post("/api/product-units", { name: this.name });
this.name = "";
this.reload();
},
async update(u) {
await common_http.put("/api/product-units/" + u.id, { name: u.name });
common_vendor.index.showToast({ title: "已保存", icon: "success" });
},
async remove(u) {
common_vendor.index.showModal({ content: "确定删除该单位?", success: async (r) => {
if (!r.confirm)
return;
await common_http.del("/api/product-units/" + u.id);
this.reload();
} });
}
}
};
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
return {
a: $data.name,
b: common_vendor.o(common_vendor.m(($event) => $data.name = $event.detail.value, {
trim: true
})),
c: common_vendor.o((...args) => $options.create && $options.create(...args)),
d: common_vendor.f($data.list, (u, k0, i0) => {
return {
a: u.name,
b: common_vendor.o(common_vendor.m(($event) => u.name = $event.detail.value, {
trim: true
}), u.id),
c: common_vendor.o(($event) => $options.update(u), u.id),
d: common_vendor.o(($event) => $options.remove(u), u.id),
e: u.id
};
})
};
}
const MiniProgramPage = /* @__PURE__ */ common_vendor._export_sfc(_sfc_main, [["render", _sfc_render]]);
wx.createPage(MiniProgramPage);
//# sourceMappingURL=../../../.sourcemap/mp-weixin/pages/product/units.js.map

View File

@@ -0,0 +1,4 @@
{
"navigationBarTitleText": "单位管理",
"usingComponents": {}
}

View File

@@ -0,0 +1 @@
<view class="page"><view class="toolbar"><input placeholder="新单位名称" value="{{a}}" bindinput="{{b}}"/><button size="mini" bindtap="{{c}}">新增</button></view><scroll-view scroll-y class="list"><view wx:for="{{d}}" wx:for-item="u" wx:key="e" class="item"><input value="{{u.a}}" bindinput="{{u.b}}"/><view class="ops"><button size="mini" bindtap="{{u.c}}">保存</button><button size="mini" type="warn" bindtap="{{u.d}}">删除</button></view></view></scroll-view></view>

View File

@@ -0,0 +1,15 @@
.page { display:flex; flex-direction: column; height: 100vh;
}
.toolbar { display:flex; gap: 12rpx; padding: 16rpx; background:#fff;
}
.toolbar input { flex:1; background:#f6f6f6; border-radius: 12rpx; padding: 12rpx;
}
.list { flex:1;
}
.item { display:flex; gap: 12rpx; align-items:center; padding: 16rpx; background:#fff; border-bottom: 1rpx solid #f1f1f1;
}
.item input { flex:1; background:#f7f7f7; border-radius: 10rpx; padding: 12rpx;
}
.ops { display:flex; gap: 10rpx;
}

View File

@@ -0,0 +1,5 @@
{
"setting": {
"urlCheck": false
}
}