Compare commits
3 Commits
158b3e65b6
...
a3bbc0098a
| Author | SHA1 | Date | |
|---|---|---|---|
| a3bbc0098a | |||
| 46c5682960 | |||
| 562ec4abf9 |
566
backend/db/db.sql
Normal file
566
backend/db/db.sql
Normal file
@@ -0,0 +1,566 @@
|
||||
-- =====================================================================
|
||||
-- 配件查询 App 数据库结构(MySQL 8.0)
|
||||
-- 依据:/doc/requirements.md、/doc/functional_spec.md、/doc/architecture.md
|
||||
-- 注意:不在此文件中创建数据库与用户,请在外部以环境配置完成
|
||||
-- 字符集统一为 utf8mb4,排序规则 utf8mb4_0900_ai_ci
|
||||
-- =====================================================================
|
||||
|
||||
SET NAMES utf8mb4;
|
||||
SET time_zone = '+00:00';
|
||||
SET sql_mode = 'STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION';
|
||||
SET FOREIGN_KEY_CHECKS = 0;
|
||||
|
||||
-- =====================================================================
|
||||
-- 基础:租户与用户
|
||||
-- =====================================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS shops (
|
||||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '店铺/租户ID',
|
||||
name VARCHAR(100) NOT NULL COMMENT '店铺名称',
|
||||
status TINYINT UNSIGNED NOT NULL DEFAULT 1 COMMENT '状态:1启用 0停用',
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
deleted_at DATETIME NULL,
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_shops_status (status)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='店铺/租户';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '用户ID',
|
||||
shop_id BIGINT UNSIGNED NOT NULL COMMENT '所属店铺',
|
||||
phone VARCHAR(32) NULL COMMENT '手机号',
|
||||
name VARCHAR(64) NOT NULL COMMENT '姓名',
|
||||
role VARCHAR(32) NOT NULL DEFAULT 'staff' COMMENT '角色:owner/staff/finance/...',
|
||||
password_hash VARCHAR(255) NULL COMMENT '密码哈希(若采用短信登录可为空)',
|
||||
status TINYINT UNSIGNED NOT NULL DEFAULT 1 COMMENT '状态:1启用 0停用',
|
||||
is_owner TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否店主',
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
deleted_at DATETIME NULL,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY ux_users_shop_phone (shop_id, phone),
|
||||
KEY idx_users_shop (shop_id),
|
||||
CONSTRAINT fk_users_shop FOREIGN KEY (shop_id) REFERENCES shops(id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户';
|
||||
|
||||
-- 第三方身份映射(微信)
|
||||
CREATE TABLE IF NOT EXISTS user_identities (
|
||||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
shop_id BIGINT UNSIGNED NOT NULL,
|
||||
user_id BIGINT UNSIGNED NOT NULL,
|
||||
provider ENUM('wechat_mp','wechat_app') NOT NULL COMMENT '身份提供方:小程序/APP',
|
||||
openid VARCHAR(64) NOT NULL,
|
||||
unionid VARCHAR(64) NULL,
|
||||
nickname VARCHAR(64) NULL,
|
||||
avatar_url VARCHAR(512) NULL,
|
||||
last_login_at DATETIME NULL,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY ux_identity_provider_openid (provider, openid),
|
||||
UNIQUE KEY ux_identity_unionid (unionid),
|
||||
KEY idx_identity_user (user_id),
|
||||
KEY idx_identity_shop (shop_id),
|
||||
CONSTRAINT fk_identity_shop FOREIGN KEY (shop_id) REFERENCES shops(id),
|
||||
CONSTRAINT fk_identity_user FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='第三方身份映射(微信)';
|
||||
|
||||
-- 微信会话(小程序/APP 临时会话)
|
||||
CREATE TABLE IF NOT EXISTS wechat_sessions (
|
||||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
provider ENUM('wechat_mp','wechat_app') NOT NULL,
|
||||
openid VARCHAR(64) NOT NULL,
|
||||
session_key VARCHAR(128) NOT NULL,
|
||||
expires_at DATETIME NOT NULL,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY ux_wechat_session (provider, openid),
|
||||
KEY idx_wechat_session_expires (expires_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='微信会话(临时)';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS system_parameters (
|
||||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
shop_id BIGINT UNSIGNED NOT NULL,
|
||||
user_id BIGINT UNSIGNED NOT NULL COMMENT '创建/最后修改人',
|
||||
`key` VARCHAR(64) NOT NULL COMMENT '参数键',
|
||||
`value` JSON NOT NULL COMMENT '参数值(JSON)',
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY ux_sysparams_shop_key (shop_id, `key`),
|
||||
KEY idx_sysparams_shop (shop_id),
|
||||
CONSTRAINT fk_sysparams_shop FOREIGN KEY (shop_id) REFERENCES shops(id),
|
||||
CONSTRAINT fk_sysparams_user FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='系统参数(租户级)';
|
||||
|
||||
-- =====================================================================
|
||||
-- 货品域(含价格/库存/图片/别名)
|
||||
-- =====================================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS product_categories (
|
||||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
shop_id BIGINT UNSIGNED NOT NULL,
|
||||
user_id BIGINT UNSIGNED NOT NULL,
|
||||
name VARCHAR(64) NOT NULL,
|
||||
parent_id BIGINT UNSIGNED NULL,
|
||||
sort_order INT NOT NULL DEFAULT 0,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
deleted_at DATETIME NULL,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY ux_categories_shop_name (shop_id, name),
|
||||
KEY idx_categories_shop (shop_id),
|
||||
KEY idx_categories_parent (parent_id),
|
||||
CONSTRAINT fk_categories_shop FOREIGN KEY (shop_id) REFERENCES shops(id),
|
||||
CONSTRAINT fk_categories_user FOREIGN KEY (user_id) REFERENCES users(id),
|
||||
CONSTRAINT fk_categories_parent FOREIGN KEY (parent_id) REFERENCES product_categories(id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='商品类别';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS product_units (
|
||||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
shop_id BIGINT UNSIGNED NOT NULL,
|
||||
user_id BIGINT UNSIGNED NOT NULL,
|
||||
name VARCHAR(16) NOT NULL,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
deleted_at DATETIME NULL,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY ux_units_shop_name (shop_id, name),
|
||||
KEY idx_units_shop (shop_id),
|
||||
CONSTRAINT fk_units_shop FOREIGN KEY (shop_id) REFERENCES shops(id),
|
||||
CONSTRAINT fk_units_user FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='商品单位';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS global_skus (
|
||||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
name VARCHAR(120) NOT NULL COMMENT 'SKU名称',
|
||||
brand VARCHAR(64) NULL,
|
||||
model VARCHAR(64) NULL,
|
||||
spec VARCHAR(128) NULL,
|
||||
barcode VARCHAR(32) NULL,
|
||||
unit_id BIGINT UNSIGNED NULL,
|
||||
tags JSON NULL,
|
||||
status ENUM('published','offline') NOT NULL DEFAULT 'published',
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
deleted_at DATETIME NULL,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY ux_global_skus_barcode (barcode),
|
||||
KEY idx_global_skus_brand_model (brand, model),
|
||||
CONSTRAINT fk_globalsku_unit FOREIGN KEY (unit_id) REFERENCES product_units(id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='全局SKU(众包)';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS products (
|
||||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
shop_id BIGINT UNSIGNED NOT NULL,
|
||||
user_id BIGINT UNSIGNED NOT NULL,
|
||||
name VARCHAR(120) NOT NULL,
|
||||
category_id BIGINT UNSIGNED NULL,
|
||||
unit_id BIGINT UNSIGNED NOT NULL,
|
||||
brand VARCHAR(64) NULL,
|
||||
model VARCHAR(64) NULL,
|
||||
spec VARCHAR(128) NULL,
|
||||
origin VARCHAR(64) NULL,
|
||||
barcode VARCHAR(32) NULL,
|
||||
alias VARCHAR(120) NULL,
|
||||
description TEXT NULL,
|
||||
global_sku_id BIGINT UNSIGNED NULL,
|
||||
safe_min DECIMAL(18,3) NULL,
|
||||
safe_max DECIMAL(18,3) NULL,
|
||||
search_text TEXT NULL COMMENT '供全文检索的聚合字段(名称/品牌/型号/规格/别名)',
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
deleted_at DATETIME NULL,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY ux_products_shop_barcode (shop_id, barcode),
|
||||
KEY idx_products_shop (shop_id),
|
||||
KEY idx_products_category (category_id),
|
||||
KEY idx_products_unit (unit_id),
|
||||
FULLTEXT KEY ft_products_search (name, brand, model, spec, search_text),
|
||||
CONSTRAINT fk_products_shop FOREIGN KEY (shop_id) REFERENCES shops(id),
|
||||
CONSTRAINT fk_products_user FOREIGN KEY (user_id) REFERENCES users(id),
|
||||
CONSTRAINT fk_products_category FOREIGN KEY (category_id) REFERENCES product_categories(id),
|
||||
CONSTRAINT fk_products_unit FOREIGN KEY (unit_id) REFERENCES product_units(id),
|
||||
CONSTRAINT fk_products_globalsku FOREIGN KEY (global_sku_id) REFERENCES global_skus(id),
|
||||
CONSTRAINT ck_products_safe_range CHECK (safe_min IS NULL OR safe_max IS NULL OR safe_min <= safe_max)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='商品';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS product_aliases (
|
||||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
shop_id BIGINT UNSIGNED NOT NULL,
|
||||
user_id BIGINT UNSIGNED NOT NULL,
|
||||
product_id BIGINT UNSIGNED NOT NULL,
|
||||
alias VARCHAR(120) NOT NULL,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
deleted_at DATETIME NULL,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY ux_product_alias (product_id, alias),
|
||||
KEY idx_product_alias_product (product_id),
|
||||
CONSTRAINT fk_alias_shop FOREIGN KEY (shop_id) REFERENCES shops(id),
|
||||
CONSTRAINT fk_alias_user FOREIGN KEY (user_id) REFERENCES users(id),
|
||||
CONSTRAINT fk_alias_product FOREIGN KEY (product_id) REFERENCES products(id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='商品别名';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS product_prices (
|
||||
product_id BIGINT UNSIGNED NOT NULL,
|
||||
shop_id BIGINT UNSIGNED NOT NULL,
|
||||
user_id BIGINT UNSIGNED NOT NULL,
|
||||
purchase_price DECIMAL(18,2) NOT NULL DEFAULT 0,
|
||||
retail_price DECIMAL(18,2) NOT NULL DEFAULT 0,
|
||||
distribution_price DECIMAL(18,2) NOT NULL DEFAULT 0,
|
||||
wholesale_price DECIMAL(18,2) NOT NULL DEFAULT 0,
|
||||
big_client_price DECIMAL(18,2) NOT NULL DEFAULT 0,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (product_id),
|
||||
KEY idx_prices_shop (shop_id),
|
||||
CONSTRAINT fk_prices_product FOREIGN KEY (product_id) REFERENCES products(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_prices_shop FOREIGN KEY (shop_id) REFERENCES shops(id),
|
||||
CONSTRAINT fk_prices_user FOREIGN KEY (user_id) REFERENCES users(id),
|
||||
CONSTRAINT ck_prices_non_negative CHECK (
|
||||
purchase_price >= 0 AND retail_price >= 0 AND distribution_price >= 0 AND wholesale_price >= 0 AND big_client_price >= 0
|
||||
)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='商品价格(含四列销售价)';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS inventories (
|
||||
product_id BIGINT UNSIGNED NOT NULL,
|
||||
shop_id BIGINT UNSIGNED NOT NULL,
|
||||
user_id BIGINT UNSIGNED NOT NULL,
|
||||
quantity DECIMAL(18,3) NOT NULL DEFAULT 0,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (product_id),
|
||||
KEY idx_inventories_shop (shop_id),
|
||||
CONSTRAINT fk_inv_product FOREIGN KEY (product_id) REFERENCES products(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_inv_shop FOREIGN KEY (shop_id) REFERENCES shops(id),
|
||||
CONSTRAINT fk_inv_user FOREIGN KEY (user_id) REFERENCES users(id),
|
||||
CONSTRAINT ck_inv_qty_non_negative CHECK (quantity >= 0)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='库存';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS product_images (
|
||||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
shop_id BIGINT UNSIGNED NOT NULL,
|
||||
user_id BIGINT UNSIGNED NOT NULL,
|
||||
product_id BIGINT UNSIGNED NOT NULL,
|
||||
url VARCHAR(512) NOT NULL,
|
||||
hash VARCHAR(64) NULL COMMENT '内容哈希(去重)',
|
||||
sort_order INT NOT NULL DEFAULT 0,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY ux_product_image_hash (product_id, hash),
|
||||
KEY idx_product_images_product (product_id),
|
||||
CONSTRAINT fk_pimg_shop FOREIGN KEY (shop_id) REFERENCES shops(id),
|
||||
CONSTRAINT fk_pimg_user FOREIGN KEY (user_id) REFERENCES users(id),
|
||||
CONSTRAINT fk_pimg_product FOREIGN KEY (product_id) REFERENCES products(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='商品图片';
|
||||
|
||||
-- =====================================================================
|
||||
-- 往来单位与账户
|
||||
-- =====================================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS customers (
|
||||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
shop_id BIGINT UNSIGNED NOT NULL,
|
||||
user_id BIGINT UNSIGNED NOT NULL,
|
||||
name VARCHAR(120) NOT NULL,
|
||||
phone VARCHAR(32) NULL,
|
||||
level VARCHAR(32) NULL COMMENT '客户等级标签',
|
||||
price_level ENUM('retail','distribution','wholesale','big_client') NOT NULL DEFAULT 'retail' COMMENT '默认售价列',
|
||||
status TINYINT UNSIGNED NOT NULL DEFAULT 1,
|
||||
remark VARCHAR(255) NULL,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
deleted_at DATETIME NULL,
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_customers_shop (shop_id),
|
||||
KEY idx_customers_phone (phone),
|
||||
CONSTRAINT fk_customers_shop FOREIGN KEY (shop_id) REFERENCES shops(id),
|
||||
CONSTRAINT fk_customers_user FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='客户';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS suppliers (
|
||||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
shop_id BIGINT UNSIGNED NOT NULL,
|
||||
user_id BIGINT UNSIGNED NOT NULL,
|
||||
name VARCHAR(120) NOT NULL,
|
||||
phone VARCHAR(32) NULL,
|
||||
status TINYINT UNSIGNED NOT NULL DEFAULT 1,
|
||||
remark VARCHAR(255) NULL,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
deleted_at DATETIME NULL,
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_suppliers_shop (shop_id),
|
||||
KEY idx_suppliers_phone (phone),
|
||||
CONSTRAINT fk_suppliers_shop FOREIGN KEY (shop_id) REFERENCES shops(id),
|
||||
CONSTRAINT fk_suppliers_user FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='供应商';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS accounts (
|
||||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
shop_id BIGINT UNSIGNED NOT NULL,
|
||||
user_id BIGINT UNSIGNED NOT NULL,
|
||||
name VARCHAR(64) NOT NULL,
|
||||
`type` ENUM('cash','bank','alipay','wechat','other') NOT NULL DEFAULT 'cash',
|
||||
balance DECIMAL(18,2) NOT NULL DEFAULT 0,
|
||||
status TINYINT UNSIGNED NOT NULL DEFAULT 1,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
deleted_at DATETIME NULL,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY ux_accounts_shop_name (shop_id, name),
|
||||
KEY idx_accounts_shop (shop_id),
|
||||
CONSTRAINT fk_accounts_shop FOREIGN KEY (shop_id) REFERENCES shops(id),
|
||||
CONSTRAINT fk_accounts_user FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='结算账户';
|
||||
|
||||
-- =====================================================================
|
||||
-- 单据域(销售/进货/其他收支/流水)
|
||||
-- =====================================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS sales_orders (
|
||||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
shop_id BIGINT UNSIGNED NOT NULL,
|
||||
user_id BIGINT UNSIGNED NOT NULL COMMENT '创建人',
|
||||
customer_id BIGINT UNSIGNED NULL,
|
||||
order_no VARCHAR(32) NOT NULL,
|
||||
order_time DATETIME NOT NULL,
|
||||
status ENUM('draft','approved','returned','void') NOT NULL DEFAULT 'draft',
|
||||
amount DECIMAL(18,2) NOT NULL DEFAULT 0 COMMENT '应收合计',
|
||||
paid_amount DECIMAL(18,2) NOT NULL DEFAULT 0 COMMENT '已收合计',
|
||||
remark VARCHAR(255) NULL,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
deleted_at DATETIME NULL,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY ux_sales_order_no (shop_id, order_no),
|
||||
KEY idx_sales_shop_time (shop_id, order_time),
|
||||
KEY idx_sales_customer (customer_id),
|
||||
CONSTRAINT fk_sales_shop FOREIGN KEY (shop_id) REFERENCES shops(id),
|
||||
CONSTRAINT fk_sales_user FOREIGN KEY (user_id) REFERENCES users(id),
|
||||
CONSTRAINT fk_sales_customer FOREIGN KEY (customer_id) REFERENCES customers(id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='销售单';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS sales_order_items (
|
||||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
order_id BIGINT UNSIGNED NOT NULL,
|
||||
product_id BIGINT UNSIGNED NOT NULL,
|
||||
quantity DECIMAL(18,3) NOT NULL,
|
||||
unit_price DECIMAL(18,2) NOT NULL,
|
||||
discount_rate DECIMAL(5,2) NOT NULL DEFAULT 0 COMMENT '折扣百分比0-100',
|
||||
amount DECIMAL(18,2) NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_soi_order (order_id),
|
||||
KEY idx_soi_product (product_id),
|
||||
CONSTRAINT fk_soi_order FOREIGN KEY (order_id) REFERENCES sales_orders(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_soi_product FOREIGN KEY (product_id) REFERENCES products(id),
|
||||
CONSTRAINT ck_soi_qty CHECK (quantity > 0),
|
||||
CONSTRAINT ck_soi_price CHECK (unit_price >= 0),
|
||||
CONSTRAINT ck_soi_discount CHECK (discount_rate >= 0 AND discount_rate <= 100)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='销售单明细';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS purchase_orders (
|
||||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
shop_id BIGINT UNSIGNED NOT NULL,
|
||||
user_id BIGINT UNSIGNED NOT NULL,
|
||||
supplier_id BIGINT UNSIGNED NULL,
|
||||
order_no VARCHAR(32) NOT NULL,
|
||||
order_time DATETIME NOT NULL,
|
||||
status ENUM('draft','approved','void') NOT NULL DEFAULT 'draft',
|
||||
amount DECIMAL(18,2) NOT NULL DEFAULT 0 COMMENT '应付合计',
|
||||
paid_amount DECIMAL(18,2) NOT NULL DEFAULT 0 COMMENT '已付合计',
|
||||
remark VARCHAR(255) NULL,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
deleted_at DATETIME NULL,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY ux_purchase_order_no (shop_id, order_no),
|
||||
KEY idx_purchase_shop_time (shop_id, order_time),
|
||||
KEY idx_purchase_supplier (supplier_id),
|
||||
CONSTRAINT fk_purchase_shop FOREIGN KEY (shop_id) REFERENCES shops(id),
|
||||
CONSTRAINT fk_purchase_user FOREIGN KEY (user_id) REFERENCES users(id),
|
||||
CONSTRAINT fk_purchase_supplier FOREIGN KEY (supplier_id) REFERENCES suppliers(id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='进货单';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS purchase_order_items (
|
||||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
order_id BIGINT UNSIGNED NOT NULL,
|
||||
product_id BIGINT UNSIGNED NOT NULL,
|
||||
quantity DECIMAL(18,3) NOT NULL,
|
||||
unit_price DECIMAL(18,2) NOT NULL,
|
||||
amount DECIMAL(18,2) NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_poi_order (order_id),
|
||||
KEY idx_poi_product (product_id),
|
||||
CONSTRAINT fk_poi_order FOREIGN KEY (order_id) REFERENCES purchase_orders(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_poi_product FOREIGN KEY (product_id) REFERENCES products(id),
|
||||
CONSTRAINT ck_poi_qty CHECK (quantity > 0),
|
||||
CONSTRAINT ck_poi_price CHECK (unit_price >= 0)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='进货单明细';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS payments (
|
||||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
shop_id BIGINT UNSIGNED NOT NULL,
|
||||
user_id BIGINT UNSIGNED NOT NULL,
|
||||
biz_type ENUM('sale','purchase','other') NOT NULL,
|
||||
biz_id BIGINT UNSIGNED NULL COMMENT '业务表ID:sales_orders/purchase_orders/other_transactions',
|
||||
account_id BIGINT UNSIGNED NOT NULL,
|
||||
direction ENUM('in','out') NOT NULL COMMENT '收款/付款',
|
||||
amount DECIMAL(18,2) NOT NULL,
|
||||
pay_time DATETIME NOT NULL,
|
||||
remark VARCHAR(255) NULL,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_payments_shop_time (shop_id, pay_time),
|
||||
KEY idx_payments_biz (biz_type, biz_id),
|
||||
CONSTRAINT fk_payments_shop FOREIGN KEY (shop_id) REFERENCES shops(id),
|
||||
CONSTRAINT fk_payments_user FOREIGN KEY (user_id) REFERENCES users(id),
|
||||
CONSTRAINT fk_payments_account FOREIGN KEY (account_id) REFERENCES accounts(id),
|
||||
CONSTRAINT ck_payments_amount CHECK (amount > 0)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='收付款记录';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS other_transactions (
|
||||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
shop_id BIGINT UNSIGNED NOT NULL,
|
||||
user_id BIGINT UNSIGNED NOT NULL,
|
||||
`type` ENUM('income','expense') NOT NULL,
|
||||
category VARCHAR(64) NOT NULL,
|
||||
counterparty_type VARCHAR(32) NULL COMMENT 'customer/supplier/other',
|
||||
counterparty_id BIGINT UNSIGNED NULL,
|
||||
account_id BIGINT UNSIGNED NOT NULL,
|
||||
amount DECIMAL(18,2) NOT NULL,
|
||||
tx_time DATETIME NOT NULL,
|
||||
remark VARCHAR(255) NULL,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
deleted_at DATETIME NULL,
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_ot_shop_time (shop_id, tx_time),
|
||||
KEY idx_ot_account (account_id),
|
||||
CONSTRAINT fk_ot_shop FOREIGN KEY (shop_id) REFERENCES shops(id),
|
||||
CONSTRAINT fk_ot_user FOREIGN KEY (user_id) REFERENCES users(id),
|
||||
CONSTRAINT fk_ot_account FOREIGN KEY (account_id) REFERENCES accounts(id),
|
||||
CONSTRAINT ck_ot_amount CHECK (amount > 0)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='其他收入/支出';
|
||||
|
||||
-- =====================================================================
|
||||
-- 配件查询与审核、附件
|
||||
-- =====================================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS part_submissions (
|
||||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
shop_id BIGINT UNSIGNED NOT NULL,
|
||||
user_id BIGINT UNSIGNED NOT NULL,
|
||||
model_unique VARCHAR(128) NOT NULL COMMENT '型号(唯一)',
|
||||
brand VARCHAR(64) NULL,
|
||||
spec VARCHAR(128) NULL,
|
||||
size VARCHAR(64) NULL,
|
||||
aperture VARCHAR(64) NULL,
|
||||
compatible TEXT NULL COMMENT '适配信息',
|
||||
status ENUM('draft','pending','rejected','published') NOT NULL DEFAULT 'pending',
|
||||
reason VARCHAR(255) NULL COMMENT '驳回原因',
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
deleted_at DATETIME NULL,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY ux_part_model_unique (model_unique),
|
||||
KEY idx_part_submissions_shop (shop_id),
|
||||
CONSTRAINT fk_part_shop FOREIGN KEY (shop_id) REFERENCES shops(id),
|
||||
CONSTRAINT fk_part_user FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='配件数据提交(审核)';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS attachments (
|
||||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
shop_id BIGINT UNSIGNED NULL COMMENT '全局资源可空,本地资源属于租户',
|
||||
user_id BIGINT UNSIGNED NULL,
|
||||
owner_type VARCHAR(32) NOT NULL COMMENT '资源归属类型:product/part_submission/global_sku/...',
|
||||
owner_id BIGINT UNSIGNED NOT NULL,
|
||||
url VARCHAR(512) NOT NULL,
|
||||
hash VARCHAR(64) NULL,
|
||||
meta JSON NULL,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY ux_attachments_hash (hash),
|
||||
KEY idx_attachments_owner (owner_type, owner_id),
|
||||
CONSTRAINT fk_att_shop FOREIGN KEY (shop_id) REFERENCES shops(id),
|
||||
CONSTRAINT fk_att_user FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='通用附件(图片等)';
|
||||
|
||||
-- =====================================================================
|
||||
-- 触发器:维护 products.search_text 聚合字段
|
||||
-- =====================================================================
|
||||
|
||||
DROP TRIGGER IF EXISTS trg_products_ai;
|
||||
DELIMITER $$
|
||||
CREATE TRIGGER trg_products_ai AFTER INSERT ON products
|
||||
FOR EACH ROW
|
||||
BEGIN
|
||||
UPDATE products
|
||||
SET search_text = CONCAT_WS(' ', NEW.name, NEW.brand, NEW.model, NEW.spec)
|
||||
WHERE id = NEW.id;
|
||||
END $$
|
||||
DELIMITER ;
|
||||
|
||||
DROP TRIGGER IF EXISTS trg_products_au;
|
||||
DELIMITER $$
|
||||
CREATE TRIGGER trg_products_au BEFORE UPDATE ON products
|
||||
FOR EACH ROW
|
||||
BEGIN
|
||||
SET NEW.search_text = CONCAT_WS(' ', NEW.name, NEW.brand, NEW.model, NEW.spec);
|
||||
END $$
|
||||
DELIMITER ;
|
||||
|
||||
-- 当别名变化时重建 search_text(名称/品牌/型号/规格 + 所有别名)
|
||||
DROP TRIGGER IF EXISTS trg_palias_ai;
|
||||
DELIMITER $$
|
||||
CREATE TRIGGER trg_palias_ai AFTER INSERT ON product_aliases
|
||||
FOR EACH ROW
|
||||
BEGIN
|
||||
UPDATE products p
|
||||
JOIN (
|
||||
SELECT pa.product_id, GROUP_CONCAT(pa.alias SEPARATOR ' ') AS aliases
|
||||
FROM product_aliases pa
|
||||
WHERE pa.product_id = NEW.product_id AND pa.deleted_at IS NULL
|
||||
GROUP BY pa.product_id
|
||||
) a ON a.product_id = p.id
|
||||
SET p.search_text = CONCAT_WS(' ', p.name, p.brand, p.model, p.spec, a.aliases)
|
||||
WHERE p.id = NEW.product_id;
|
||||
END $$
|
||||
DELIMITER ;
|
||||
|
||||
DROP TRIGGER IF EXISTS trg_palias_au;
|
||||
DELIMITER $$
|
||||
CREATE TRIGGER trg_palias_au AFTER UPDATE ON product_aliases
|
||||
FOR EACH ROW
|
||||
BEGIN
|
||||
UPDATE products p
|
||||
JOIN (
|
||||
SELECT pa.product_id, GROUP_CONCAT(pa.alias SEPARATOR ' ') AS aliases
|
||||
FROM product_aliases pa
|
||||
WHERE pa.product_id = NEW.product_id AND pa.deleted_at IS NULL
|
||||
GROUP BY pa.product_id
|
||||
) a ON a.product_id = p.id
|
||||
SET p.search_text = CONCAT_WS(' ', p.name, p.brand, p.model, p.spec, a.aliases)
|
||||
WHERE p.id = NEW.product_id;
|
||||
END $$
|
||||
DELIMITER ;
|
||||
|
||||
DROP TRIGGER IF EXISTS trg_palias_ad;
|
||||
DELIMITER $$
|
||||
CREATE TRIGGER trg_palias_ad AFTER DELETE ON product_aliases
|
||||
FOR EACH ROW
|
||||
BEGIN
|
||||
UPDATE products p
|
||||
LEFT JOIN (
|
||||
SELECT pa.product_id, GROUP_CONCAT(pa.alias SEPARATOR ' ') AS aliases
|
||||
FROM product_aliases pa
|
||||
WHERE pa.product_id = OLD.product_id AND pa.deleted_at IS NULL
|
||||
GROUP BY pa.product_id
|
||||
) a ON a.product_id = p.id
|
||||
SET p.search_text = CONCAT_WS(' ', p.name, p.brand, p.model, p.spec, COALESCE(a.aliases, ''))
|
||||
WHERE p.id = OLD.product_id;
|
||||
END $$
|
||||
DELIMITER ;
|
||||
|
||||
SET FOREIGN_KEY_CHECKS = 1;
|
||||
|
||||
|
||||
77
backend/doc/同步文档.md
Normal file
77
backend/doc/同步文档.md
Normal file
@@ -0,0 +1,77 @@
|
||||
## 前后端数据库状态说明
|
||||
|
||||
**更新日期**: 2025-09-17
|
||||
|
||||
### 概要
|
||||
- 数据库已落地:已在远程 MySQL `mysql.tonaspace.com` 的 `partsinquiry` 库完成初始化(表结构与触发器已创建)。
|
||||
- 已生成根目录文档:`/doc/database_documentation.md` 已同步线上结构(字段、索引、外键、触发器)。
|
||||
- 后端代码仍未配置数据源依赖与连接,前端无本地结构化存储方案。
|
||||
|
||||
### 已建库与连接信息(用于部署/联调)
|
||||
- Address: `mysql.tonaspace.com`
|
||||
- Database: `partsinquiry`
|
||||
- User: `root`
|
||||
- 说明:所有结构变更均通过 MysqlMCP 执行并已落地到线上库。
|
||||
|
||||
### 角色与模拟数据策略(统一为店长)
|
||||
- 当前不进行角色划分,系统仅保留“店长”角色。
|
||||
- 已将所有用户记录统一为:`role='owner'`、`is_owner=1`。
|
||||
- 前端/后端权限逻辑暂未启用,后续若引入权限体系,再行扩展角色与边界。
|
||||
|
||||
### 小程序默认用户(可开关,默认关闭)
|
||||
- 目的:开发/演示阶段,便于免登录联调。
|
||||
- 机制:前端在请求头附加 `X-User-Id`(值为张老板 id=2),仅当开关开启时。
|
||||
- 开关:
|
||||
- 环境变量:`VITE_APP_ENABLE_DEFAULT_USER=true` 与 `VITE_APP_DEFAULT_USER_ID=2`
|
||||
- 或本地存储:`ENABLE_DEFAULT_USER=true` 与 `DEFAULT_USER_ID=2`
|
||||
- 关闭:不设置/置为 `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`。
|
||||
- 配置:`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/云数据库使用;页面数据为内置静态数据。
|
||||
- 本地存储:未见 `uni.setStorage`/`uni.getStorage` 的集中封装或结构化键空间设计。
|
||||
- 结论:前端当前不涉及本地数据库或结构化存储方案。
|
||||
|
||||
### 风险与影响
|
||||
- 后端未配置数据源与接口,应用无法读写远端库(虽已建表)。
|
||||
- 无接口契约,前后端仍无法联调涉及数据库的功能。
|
||||
|
||||
### 建议的后续行动(不自动执行)
|
||||
- 在后端引入依赖:`spring-boot-starter-web`、`spring-boot-starter-data-jpa`、`mysql-connector-j`。
|
||||
- 配置数据源:使用环境变量注入 `SPRING_DATASOURCE_URL`、`SPRING_DATASOURCE_USERNAME`、`SPRING_DATASOURCE_PASSWORD` 等,指向上述远程库。
|
||||
- 引入迁移工具(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' }`
|
||||
- 商品列表/详情展示该占位图地址
|
||||
|
||||
|
||||
|
||||
BIN
backend/picture/屏幕截图 2025-08-14 134657.png
Normal file
BIN
backend/picture/屏幕截图 2025-08-14 134657.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
@@ -35,6 +35,25 @@
|
||||
<artifactId>spring-boot-starter</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Web MVC -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- JPA for MySQL access -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- MySQL Driver -->
|
||||
<dependency>
|
||||
<groupId>com.mysql</groupId>
|
||||
<artifactId>mysql-connector-j</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.example.demo.dashboard;
|
||||
|
||||
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.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/dashboard")
|
||||
public class DashboardController {
|
||||
|
||||
private final DashboardService dashboardService;
|
||||
|
||||
public DashboardController(DashboardService dashboardService) {
|
||||
this.dashboardService = dashboardService;
|
||||
}
|
||||
|
||||
@GetMapping("/overview")
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.example.demo.dashboard;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
public class DashboardOverviewResponse {
|
||||
private BigDecimal todaySalesAmount;
|
||||
private BigDecimal monthSalesAmount;
|
||||
private BigDecimal monthGrossProfit;
|
||||
private BigDecimal stockTotalQuantity;
|
||||
|
||||
public DashboardOverviewResponse(BigDecimal todaySalesAmount, BigDecimal monthSalesAmount, BigDecimal monthGrossProfit, BigDecimal stockTotalQuantity) {
|
||||
this.todaySalesAmount = todaySalesAmount;
|
||||
this.monthSalesAmount = monthSalesAmount;
|
||||
this.monthGrossProfit = monthGrossProfit;
|
||||
this.stockTotalQuantity = stockTotalQuantity;
|
||||
}
|
||||
|
||||
public BigDecimal getTodaySalesAmount() {
|
||||
return todaySalesAmount;
|
||||
}
|
||||
|
||||
public BigDecimal getMonthGrossProfit() {
|
||||
return monthGrossProfit;
|
||||
}
|
||||
|
||||
public BigDecimal getStockTotalQuantity() {
|
||||
return stockTotalQuantity;
|
||||
}
|
||||
|
||||
public BigDecimal getMonthSalesAmount() {
|
||||
return monthSalesAmount;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
package com.example.demo.dashboard;
|
||||
|
||||
import jakarta.persistence.EntityManager;
|
||||
import jakarta.persistence.PersistenceContext;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
@Repository
|
||||
public class DashboardRepository {
|
||||
|
||||
@PersistenceContext
|
||||
private EntityManager entityManager;
|
||||
|
||||
public BigDecimal sumTodaySalesOrders(Long shopId) {
|
||||
Object result = entityManager.createNativeQuery(
|
||||
"SELECT COALESCE(SUM(amount), 0) FROM sales_orders " +
|
||||
"WHERE shop_id = :shopId AND status = 'approved' AND DATE(order_time) = CURRENT_DATE()"
|
||||
).setParameter("shopId", shopId).getSingleResult();
|
||||
return toBigDecimal(result);
|
||||
}
|
||||
|
||||
public BigDecimal sumMonthGrossProfitApprox(Long shopId) {
|
||||
Object result = entityManager.createNativeQuery(
|
||||
"SELECT COALESCE(SUM(soi.amount - soi.quantity * COALESCE(pp.purchase_price, 0)), 0) AS gp " +
|
||||
"FROM sales_orders so " +
|
||||
"JOIN sales_order_items soi ON soi.order_id = so.id " +
|
||||
"LEFT JOIN product_prices pp ON pp.product_id = soi.product_id AND pp.shop_id = so.shop_id " +
|
||||
"WHERE so.shop_id = :shopId AND so.status = 'approved' " +
|
||||
"AND so.order_time >= DATE_FORMAT(CURRENT_DATE(), '%Y-%m-01') " +
|
||||
"AND so.order_time < DATE_ADD(DATE_FORMAT(CURRENT_DATE(), '%Y-%m-01'), INTERVAL 1 MONTH)"
|
||||
).setParameter("shopId", shopId).getSingleResult();
|
||||
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"
|
||||
).setParameter("shopId", shopId).getSingleResult();
|
||||
return toBigDecimal(result);
|
||||
}
|
||||
|
||||
private BigDecimal toBigDecimal(Object value) {
|
||||
if (value == null) return BigDecimal.ZERO;
|
||||
if (value instanceof BigDecimal) return (BigDecimal) value;
|
||||
if (value instanceof Number) return BigDecimal.valueOf(((Number) value).doubleValue());
|
||||
try {
|
||||
return new BigDecimal(value.toString());
|
||||
} catch (Exception e) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.example.demo.dashboard;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
@Service
|
||||
public class DashboardService {
|
||||
private final DashboardRepository dashboardRepository;
|
||||
|
||||
public DashboardService(DashboardRepository dashboardRepository) {
|
||||
this.dashboardRepository = dashboardRepository;
|
||||
}
|
||||
|
||||
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, 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
82
backend/src/main/java/com/example/demo/notice/Notice.java
Normal file
82
backend/src/main/java/com/example/demo/notice/Notice.java
Normal file
@@ -0,0 +1,82 @@
|
||||
package com.example.demo.notice;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Entity
|
||||
@Table(name = "notices")
|
||||
public class Notice {
|
||||
@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 = "title", nullable = false, length = 120)
|
||||
private String title;
|
||||
|
||||
@Column(name = "content", nullable = false, length = 500)
|
||||
private String content;
|
||||
|
||||
@Column(name = "tag", length = 32)
|
||||
private String tag;
|
||||
|
||||
@Column(name = "is_pinned", nullable = false)
|
||||
private Boolean pinned = false;
|
||||
|
||||
@Column(name = "starts_at")
|
||||
private LocalDateTime startsAt;
|
||||
|
||||
@Column(name = "ends_at")
|
||||
private LocalDateTime endsAt;
|
||||
|
||||
@Convert(converter = NoticeStatusConverter.class)
|
||||
@Column(name = "status", nullable = false, length = 16)
|
||||
private NoticeStatus status = NoticeStatus.PUBLISHED;
|
||||
|
||||
@Column(name = "created_at", nullable = false, updatable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@Column(name = "updated_at", nullable = false)
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
@PrePersist
|
||||
protected void onCreate() {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
this.createdAt = now;
|
||||
this.updatedAt = now;
|
||||
}
|
||||
|
||||
@PreUpdate
|
||||
protected void onUpdate() {
|
||||
this.updatedAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
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 getTitle() { return title; }
|
||||
public void setTitle(String title) { this.title = title; }
|
||||
public String getContent() { return content; }
|
||||
public void setContent(String content) { this.content = content; }
|
||||
public String getTag() { return tag; }
|
||||
public void setTag(String tag) { this.tag = tag; }
|
||||
public Boolean getPinned() { return pinned; }
|
||||
public void setPinned(Boolean pinned) { this.pinned = pinned; }
|
||||
public LocalDateTime getStartsAt() { return startsAt; }
|
||||
public void setStartsAt(LocalDateTime startsAt) { this.startsAt = startsAt; }
|
||||
public LocalDateTime getEndsAt() { return endsAt; }
|
||||
public void setEndsAt(LocalDateTime endsAt) { this.endsAt = endsAt; }
|
||||
public NoticeStatus getStatus() { return status; }
|
||||
public void setStatus(NoticeStatus status) { this.status = status; }
|
||||
public LocalDateTime getCreatedAt() { return createdAt; }
|
||||
public LocalDateTime getUpdatedAt() { return updatedAt; }
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.example.demo.notice;
|
||||
|
||||
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.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/notices")
|
||||
public class NoticeController {
|
||||
|
||||
private final NoticeService noticeService;
|
||||
|
||||
public NoticeController(NoticeService noticeService) {
|
||||
this.noticeService = noticeService;
|
||||
}
|
||||
|
||||
/**
|
||||
* 简化:通过请求头 X-Shop-Id 传递当前店铺ID。
|
||||
*/
|
||||
@GetMapping
|
||||
public ResponseEntity<List<Notice>> list(@RequestHeader(name = "X-Shop-Id", required = false) Long shopId) {
|
||||
Long sid = (shopId == null ? 1L : shopId);
|
||||
return ResponseEntity.ok(noticeService.listActive(sid));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.example.demo.notice;
|
||||
|
||||
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 NoticeRepository extends JpaRepository<Notice, Long> {
|
||||
|
||||
@Query("SELECT n FROM Notice n WHERE n.shopId = :shopId AND n.status = :status " +
|
||||
"AND (n.startsAt IS NULL OR n.startsAt <= CURRENT_TIMESTAMP) AND (n.endsAt IS NULL OR n.endsAt >= CURRENT_TIMESTAMP) " +
|
||||
"ORDER BY n.pinned DESC, n.createdAt DESC")
|
||||
List<Notice> findActiveNotices(@Param("shopId") Long shopId, @Param("status") NoticeStatus status);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.example.demo.notice;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
public class NoticeService {
|
||||
private final NoticeRepository noticeRepository;
|
||||
|
||||
public NoticeService(NoticeRepository noticeRepository) {
|
||||
this.noticeRepository = noticeRepository;
|
||||
}
|
||||
|
||||
public List<Notice> listActive(Long shopId) {
|
||||
return noticeRepository.findActiveNotices(shopId, NoticeStatus.PUBLISHED);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.example.demo.notice;
|
||||
|
||||
/**
|
||||
* 公告状态。
|
||||
*/
|
||||
public enum NoticeStatus {
|
||||
DRAFT,
|
||||
PUBLISHED,
|
||||
OFFLINE
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.example.demo.notice;
|
||||
|
||||
import jakarta.persistence.AttributeConverter;
|
||||
import jakarta.persistence.Converter;
|
||||
|
||||
@Converter(autoApply = false)
|
||||
public class NoticeStatusConverter implements AttributeConverter<NoticeStatus, String> {
|
||||
@Override
|
||||
public String convertToDatabaseColumn(NoticeStatus attribute) {
|
||||
if (attribute == null) return null;
|
||||
return switch (attribute) {
|
||||
case DRAFT -> "draft";
|
||||
case PUBLISHED -> "published";
|
||||
case OFFLINE -> "offline";
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public NoticeStatus convertToEntityAttribute(String dbData) {
|
||||
if (dbData == null) return null;
|
||||
return switch (dbData) {
|
||||
case "draft" -> NoticeStatus.DRAFT;
|
||||
case "published" -> NoticeStatus.PUBLISHED;
|
||||
case "offline" -> NoticeStatus.OFFLINE;
|
||||
default -> NoticeStatus.PUBLISHED;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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> {
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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> {
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
|
||||
@@ -1 +1,35 @@
|
||||
spring.application.name=demo
|
||||
|
||||
# 数据源配置(通过环境变量注入,避免硬编码)
|
||||
spring.datasource.url=${SPRING_DATASOURCE_URL}
|
||||
spring.datasource.username=${SPRING_DATASOURCE_USERNAME}
|
||||
spring.datasource.password=${SPRING_DATASOURCE_PASSWORD}
|
||||
|
||||
# JPA 基本配置
|
||||
spring.jpa.hibernate.ddl-auto=none
|
||||
spring.jpa.open-in-view=false
|
||||
spring.jpa.show-sql=false
|
||||
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect
|
||||
|
||||
# CORS 简单放开(如需跨域)
|
||||
spring.web.cors.allowed-origins=${CORS_ALLOWED_ORIGINS:*}
|
||||
spring.web.cors.allowed-methods=GET,POST,PUT,DELETE,OPTIONS
|
||||
spring.web.cors.allowed-headers=*
|
||||
|
||||
# Hikari 连接池保活(避免云数据库空闲断开)
|
||||
spring.datasource.hikari.maximum-pool-size=10
|
||||
spring.datasource.hikari.minimum-idle=2
|
||||
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}
|
||||
|
||||
408
doc/database_documentation.md
Normal file
408
doc/database_documentation.md
Normal file
@@ -0,0 +1,408 @@
|
||||
## partsinquiry 数据库文档
|
||||
|
||||
更新日期:2025-09-16(已插入演示数据)
|
||||
|
||||
说明:本文件根据远程库 mysql.tonaspace.com 中 `partsinquiry` 的实际结构生成,字段/索引/外键信息以线上为准。
|
||||
|
||||
### shops
|
||||
| Column Name | Data Type | Nullable | Default | Comment |
|
||||
| ----------- | --------- | -------- | ------- | ------- |
|
||||
| id | BIGINT UNSIGNED | NOT NULL | AUTO_INCREMENT | 店铺/租户ID |
|
||||
| name | VARCHAR(100) | NOT NULL | | 店铺名称 |
|
||||
| status | TINYINT UNSIGNED | NOT NULL | 1 | 状态:1启用 0停用 |
|
||||
| created_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | |
|
||||
| updated_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | |
|
||||
| deleted_at | DATETIME | YES | | |
|
||||
|
||||
**Indexes**: - PRIMARY KEY: `id` - KEY: `idx_shops_status` (`status`)
|
||||
|
||||
### users
|
||||
| Column Name | Data Type | Nullable | Default | Comment |
|
||||
| ----------- | --------- | -------- | ------- | ------- |
|
||||
| id | BIGINT UNSIGNED | NOT NULL | AUTO_INCREMENT | 用户ID |
|
||||
| shop_id | BIGINT UNSIGNED | NOT NULL | | 所属店铺 |
|
||||
| phone | VARCHAR(32) | YES | | 手机号 |
|
||||
| name | VARCHAR(64) | NOT NULL | | 姓名 |
|
||||
| role | VARCHAR(32) | NOT NULL | staff | 角色:owner/staff/finance/... |
|
||||
| password_hash | VARCHAR(255) | YES | | 密码哈希(若采用短信登录可为空) |
|
||||
| status | TINYINT UNSIGNED | NOT NULL | 1 | 状态:1启用 0停用 |
|
||||
| is_owner | TINYINT(1) | NOT NULL | 0 | 是否店主 |
|
||||
| created_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | |
|
||||
| updated_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | |
|
||||
| deleted_at | DATETIME | YES | | |
|
||||
|
||||
**Indexes**: - PRIMARY KEY: `id` - KEY: `idx_users_shop` (`shop_id`) - UNIQUE: `ux_users_shop_phone` (`shop_id`,`phone`)
|
||||
**Foreign Keys**: - `fk_users_shop`: `shop_id` → `shops(id)` ON UPDATE NO ACTION ON DELETE NO ACTION
|
||||
|
||||
### user_identities
|
||||
| Column Name | Data Type | Nullable | Default | Comment |
|
||||
| ----------- | --------- | -------- | ------- | ------- |
|
||||
| id | BIGINT UNSIGNED | NOT NULL | AUTO_INCREMENT | |
|
||||
| shop_id | BIGINT UNSIGNED | NOT NULL | | |
|
||||
| user_id | BIGINT UNSIGNED | NOT NULL | | |
|
||||
| provider | ENUM('wechat_mp','wechat_app') | NOT NULL | | 身份提供方:小程序/APP |
|
||||
| openid | VARCHAR(64) | NOT NULL | | |
|
||||
| unionid | VARCHAR(64) | YES | | |
|
||||
| nickname | VARCHAR(64) | YES | | |
|
||||
| avatar_url | VARCHAR(512) | YES | | |
|
||||
| last_login_at | DATETIME | YES | | |
|
||||
| created_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | |
|
||||
| updated_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | |
|
||||
|
||||
**Indexes**: - PRIMARY KEY: `id` - KEY: `idx_identity_shop` (`shop_id`) - KEY: `idx_identity_user` (`user_id`) - UNIQUE: `ux_identity_provider_openid` (`provider`,`openid`) - UNIQUE: `ux_identity_unionid` (`unionid`)
|
||||
**Foreign Keys**: - `fk_identity_shop`: `shop_id` → `shops(id)` ON UPDATE NO ACTION ON DELETE NO ACTION - `fk_identity_user`: `user_id` → `users(id)` ON UPDATE NO ACTION ON DELETE NO ACTION
|
||||
|
||||
### wechat_sessions
|
||||
| Column Name | Data Type | Nullable | Default | Comment |
|
||||
| ----------- | --------- | -------- | ------- | ------- |
|
||||
| id | BIGINT UNSIGNED | NOT NULL | AUTO_INCREMENT | |
|
||||
| provider | ENUM('wechat_mp','wechat_app') | NOT NULL | | |
|
||||
| openid | VARCHAR(64) | NOT NULL | | |
|
||||
| session_key | VARCHAR(128) | NOT NULL | | |
|
||||
| expires_at | DATETIME | NOT NULL | | |
|
||||
| created_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | |
|
||||
|
||||
**Indexes**: - PRIMARY KEY: `id` - KEY: `idx_wechat_session_expires` (`expires_at`) - UNIQUE: `ux_wechat_session` (`provider`,`openid`)
|
||||
|
||||
### system_parameters
|
||||
| Column Name | Data Type | Nullable | Default | Comment |
|
||||
| ----------- | --------- | -------- | ------- | ------- |
|
||||
| id | BIGINT UNSIGNED | NOT NULL | AUTO_INCREMENT | |
|
||||
| shop_id | BIGINT UNSIGNED | NOT NULL | | |
|
||||
| user_id | BIGINT UNSIGNED | NOT NULL | | 创建/最后修改人 |
|
||||
| key | VARCHAR(64) | NOT NULL | | 参数键 |
|
||||
| value | JSON | NOT NULL | | 参数值(JSON) |
|
||||
| created_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | |
|
||||
| updated_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | |
|
||||
|
||||
**Indexes**: - PRIMARY KEY: `id` - KEY: `idx_sysparams_shop` (`shop_id`) - UNIQUE: `ux_sysparams_shop_key` (`shop_id`,`key`)
|
||||
**Foreign Keys**: - `fk_sysparams_shop`: `shop_id` → `shops(id)` ON UPDATE NO ACTION ON DELETE NO ACTION - `fk_sysparams_user`: `user_id` → `users(id)` ON UPDATE NO ACTION ON DELETE NO ACTION
|
||||
|
||||
### product_units
|
||||
| Column Name | Data Type | Nullable | Default | Comment |
|
||||
| ----------- | --------- | -------- | ------- | ------- |
|
||||
| id | BIGINT UNSIGNED | NOT NULL | AUTO_INCREMENT | |
|
||||
| shop_id | BIGINT UNSIGNED | NOT NULL | | |
|
||||
| user_id | BIGINT UNSIGNED | NOT NULL | | |
|
||||
| name | VARCHAR(16) | NOT NULL | | |
|
||||
| created_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | |
|
||||
| updated_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | |
|
||||
| deleted_at | DATETIME | YES | | |
|
||||
|
||||
**Indexes**: - PRIMARY KEY: `id` - KEY: `idx_units_shop` (`shop_id`) - UNIQUE: `ux_units_shop_name` (`shop_id`,`name`)
|
||||
**Foreign Keys**: - `fk_units_shop`: `shop_id` → `shops(id)` ON UPDATE NO ACTION ON DELETE NO ACTION - `fk_units_user`: `user_id` → `users(id)` ON UPDATE NO ACTION ON DELETE NO ACTION
|
||||
|
||||
### global_skus
|
||||
| Column Name | Data Type | Nullable | Default | Comment |
|
||||
| ----------- | --------- | -------- | ------- | ------- |
|
||||
| id | BIGINT UNSIGNED | NOT NULL | AUTO_INCREMENT | |
|
||||
| name | VARCHAR(120) | NOT NULL | | SKU名称 |
|
||||
| brand | VARCHAR(64) | YES | | |
|
||||
| model | VARCHAR(64) | YES | | |
|
||||
| spec | VARCHAR(128) | YES | | |
|
||||
| barcode | VARCHAR(32) | YES | | |
|
||||
| unit_id | BIGINT UNSIGNED | YES | | |
|
||||
| tags | JSON | YES | | |
|
||||
| status | ENUM('published','offline') | NOT NULL | published | |
|
||||
| created_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | |
|
||||
| updated_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | |
|
||||
| deleted_at | DATETIME | YES | | |
|
||||
|
||||
**Indexes**: - PRIMARY KEY: `id` - KEY: `idx_global_skus_brand_model` (`brand`,`model`) - UNIQUE: `ux_global_skus_barcode` (`barcode`)
|
||||
**Foreign Keys**: - `fk_globalsku_unit`: `unit_id` → `product_units(id)` ON UPDATE NO ACTION ON DELETE NO ACTION
|
||||
|
||||
### product_categories
|
||||
| Column Name | Data Type | Nullable | Default | Comment |
|
||||
| ----------- | --------- | -------- | ------- | ------- |
|
||||
| id | BIGINT UNSIGNED | NOT NULL | AUTO_INCREMENT | |
|
||||
| shop_id | BIGINT UNSIGNED | NOT NULL | | |
|
||||
| user_id | BIGINT UNSIGNED | NOT NULL | | |
|
||||
| name | VARCHAR(64) | NOT NULL | | |
|
||||
| parent_id | BIGINT UNSIGNED | YES | | |
|
||||
| sort_order | INT | NOT NULL | 0 | |
|
||||
| created_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | |
|
||||
| updated_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | |
|
||||
| deleted_at | DATETIME | YES | | |
|
||||
|
||||
**Indexes**: - PRIMARY KEY: `id` - KEY: `idx_categories_shop` (`shop_id`) - KEY: `idx_categories_parent` (`parent_id`) - UNIQUE: `ux_categories_shop_name` (`shop_id`,`name`)
|
||||
**Foreign Keys**: - `fk_categories_shop`: `shop_id` → `shops(id)` ON UPDATE NO ACTION ON DELETE NO ACTION - `fk_categories_user`: `user_id` → `users(id)` ON UPDATE NO ACTION ON DELETE NO ACTION - `fk_categories_parent`: `parent_id` → `product_categories(id)` ON UPDATE NO ACTION ON DELETE NO ACTION
|
||||
|
||||
### products
|
||||
| Column Name | Data Type | Nullable | Default | Comment |
|
||||
| ----------- | --------- | -------- | ------- | ------- |
|
||||
| id | BIGINT UNSIGNED | NOT NULL | AUTO_INCREMENT | |
|
||||
| shop_id | BIGINT UNSIGNED | NOT NULL | | |
|
||||
| user_id | BIGINT UNSIGNED | NOT NULL | | |
|
||||
| name | VARCHAR(120) | NOT NULL | | 供全文检索 |
|
||||
| category_id | BIGINT UNSIGNED | YES | | |
|
||||
| unit_id | BIGINT UNSIGNED | NOT NULL | | |
|
||||
| brand | VARCHAR(64) | YES | | |
|
||||
| model | VARCHAR(64) | YES | | |
|
||||
| spec | VARCHAR(128) | YES | | |
|
||||
| origin | VARCHAR(64) | YES | | |
|
||||
| barcode | VARCHAR(32) | YES | | |
|
||||
| alias | VARCHAR(120) | YES | | |
|
||||
| description | TEXT | YES | | |
|
||||
| global_sku_id | BIGINT UNSIGNED | YES | | |
|
||||
| safe_min | DECIMAL(18,3) | YES | | |
|
||||
| safe_max | DECIMAL(18,3) | YES | | |
|
||||
| search_text | TEXT | YES | | 供全文检索的聚合字段(名称/品牌/型号/规格/别名) |
|
||||
| created_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | |
|
||||
| updated_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | |
|
||||
| deleted_at | DATETIME | YES | | |
|
||||
|
||||
**Indexes**: - PRIMARY KEY: `id` - KEY: `idx_products_shop` (`shop_id`) - KEY: `idx_products_category` (`category_id`) - KEY: `idx_products_unit` (`unit_id`) - FULLTEXT: `ft_products_search` (`name`,`brand`,`model`,`spec`,`search_text`) - UNIQUE: `ux_products_shop_barcode` (`shop_id`,`barcode`)
|
||||
**Foreign Keys**: - `fk_products_shop`: `shop_id` → `shops(id)` - `fk_products_user`: `user_id` → `users(id)` - `fk_products_category`: `category_id` → `product_categories(id)` - `fk_products_unit`: `unit_id` → `product_units(id)` - `fk_products_globalsku`: `global_sku_id` → `global_skus(id)`
|
||||
|
||||
### product_aliases
|
||||
| Column Name | Data Type | Nullable | Default | Comment |
|
||||
| ----------- | --------- | -------- | ------- | ------- |
|
||||
| id | BIGINT UNSIGNED | NOT NULL | AUTO_INCREMENT | |
|
||||
| shop_id | BIGINT UNSIGNED | NOT NULL | | |
|
||||
| user_id | BIGINT UNSIGNED | NOT NULL | | |
|
||||
| product_id | BIGINT UNSIGNED | NOT NULL | | |
|
||||
| alias | VARCHAR(120) | NOT NULL | | |
|
||||
| created_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | |
|
||||
| updated_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | |
|
||||
| deleted_at | DATETIME | YES | | |
|
||||
|
||||
**Indexes**: - PRIMARY KEY: `id` - KEY: `idx_product_alias_product` (`product_id`) - UNIQUE: `ux_product_alias` (`product_id`,`alias`)
|
||||
**Foreign Keys**: - `fk_alias_shop`: `shop_id` → `shops(id)` - `fk_alias_user`: `user_id` → `users(id)` - `fk_alias_product`: `product_id` → `products(id)`
|
||||
|
||||
### product_images
|
||||
| Column Name | Data Type | Nullable | Default | Comment |
|
||||
| ----------- | --------- | -------- | ------- | ------- |
|
||||
| id | BIGINT UNSIGNED | NOT NULL | AUTO_INCREMENT | |
|
||||
| shop_id | BIGINT UNSIGNED | NOT NULL | | |
|
||||
| user_id | BIGINT UNSIGNED | NOT NULL | | |
|
||||
| product_id | BIGINT UNSIGNED | NOT NULL | | |
|
||||
| url | VARCHAR(512) | NOT NULL | | |
|
||||
| hash | VARCHAR(64) | YES | | 内容哈希(去重) |
|
||||
| sort_order | INT | NOT NULL | 0 | |
|
||||
| created_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | |
|
||||
|
||||
**Indexes**: - PRIMARY KEY: `id` - KEY: `idx_product_images_product` (`product_id`) - UNIQUE: `ux_product_image_hash` (`product_id`,`hash`)
|
||||
**Foreign Keys**: - `fk_pimg_shop`: `shop_id` → `shops(id)` - `fk_pimg_user`: `user_id` → `users(id)` - `fk_pimg_product`: `product_id` → `products(id)` ON DELETE CASCADE
|
||||
|
||||
### product_prices
|
||||
| Column Name | Data Type | Nullable | Default | Comment |
|
||||
| ----------- | --------- | -------- | ------- | ------- |
|
||||
| product_id | BIGINT UNSIGNED | NOT NULL | | |
|
||||
| shop_id | BIGINT UNSIGNED | NOT NULL | | |
|
||||
| user_id | BIGINT UNSIGNED | NOT NULL | | |
|
||||
| purchase_price | DECIMAL(18,2) | NOT NULL | 0.00 | |
|
||||
| retail_price | DECIMAL(18,2) | NOT NULL | 0.00 | |
|
||||
| distribution_price | DECIMAL(18,2) | NOT NULL | 0.00 | |
|
||||
| wholesale_price | DECIMAL(18,2) | NOT NULL | 0.00 | |
|
||||
| big_client_price | DECIMAL(18,2) | NOT NULL | 0.00 | |
|
||||
| updated_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | |
|
||||
|
||||
**Indexes**: - PRIMARY KEY: `product_id` - KEY: `idx_prices_shop` (`shop_id`)
|
||||
**Foreign Keys**: - `fk_prices_product`: `product_id` → `products(id)` ON DELETE CASCADE - `fk_prices_shop`: `shop_id` → `shops(id)` - `fk_prices_user`: `user_id` → `users(id)`
|
||||
|
||||
### inventories
|
||||
| Column Name | Data Type | Nullable | Default | Comment |
|
||||
| ----------- | --------- | -------- | ------- | ------- |
|
||||
| product_id | BIGINT UNSIGNED | NOT NULL | | |
|
||||
| shop_id | BIGINT UNSIGNED | NOT NULL | | |
|
||||
| user_id | BIGINT UNSIGNED | NOT NULL | | |
|
||||
| quantity | DECIMAL(18,3) | NOT NULL | 0.000 | |
|
||||
| updated_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | |
|
||||
|
||||
**Indexes**: - PRIMARY KEY: `product_id` - KEY: `idx_inventories_shop` (`shop_id`)
|
||||
**Foreign Keys**: - `fk_inv_product`: `product_id` → `products(id)` ON DELETE CASCADE - `fk_inv_shop`: `shop_id` → `shops(id)` - `fk_inv_user`: `user_id` → `users(id)`
|
||||
|
||||
### customers
|
||||
| Column Name | Data Type | Nullable | Default | Comment |
|
||||
| ----------- | --------- | -------- | ------- | ------- |
|
||||
| id | BIGINT UNSIGNED | NOT NULL | AUTO_INCREMENT | |
|
||||
| shop_id | BIGINT UNSIGNED | NOT NULL | | |
|
||||
| user_id | BIGINT UNSIGNED | NOT NULL | | |
|
||||
| name | VARCHAR(120) | NOT NULL | | |
|
||||
| phone | VARCHAR(32) | YES | | |
|
||||
| level | VARCHAR(32) | YES | | 客户等级标签 |
|
||||
| price_level | ENUM('retail','distribution','wholesale','big_client') | NOT NULL | retail | 默认售价列 |
|
||||
| status | TINYINT UNSIGNED | NOT NULL | 1 | |
|
||||
| remark | VARCHAR(255) | YES | | |
|
||||
| created_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | |
|
||||
| updated_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | |
|
||||
| deleted_at | DATETIME | YES | | |
|
||||
|
||||
**Indexes**: - PRIMARY KEY: `id` - KEY: `idx_customers_shop` (`shop_id`) - KEY: `idx_customers_phone` (`phone`)
|
||||
**Foreign Keys**: - `fk_customers_shop`: `shop_id` → `shops(id)` - `fk_customers_user`: `user_id` → `users(id)`
|
||||
|
||||
### suppliers
|
||||
| Column Name | Data Type | Nullable | Default | Comment |
|
||||
| ----------- | --------- | -------- | ------- | ------- |
|
||||
| id | BIGINT UNSIGNED | NOT NULL | AUTO_INCREMENT | |
|
||||
| shop_id | BIGINT UNSIGNED | NOT NULL | | |
|
||||
| user_id | BIGINT UNSIGNED | NOT NULL | | |
|
||||
| name | VARCHAR(120) | NOT NULL | | |
|
||||
| phone | VARCHAR(32) | YES | | |
|
||||
| status | TINYINT UNSIGNED | NOT NULL | 1 | |
|
||||
| remark | VARCHAR(255) | YES | | |
|
||||
| created_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | |
|
||||
| updated_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | |
|
||||
| deleted_at | DATETIME | YES | | |
|
||||
|
||||
**Indexes**: - PRIMARY KEY: `id` - KEY: `idx_suppliers_shop` (`shop_id`) - KEY: `idx_suppliers_phone` (`phone`)
|
||||
**Foreign Keys**: - `fk_suppliers_shop`: `shop_id` → `shops(id)` - `fk_suppliers_user`: `user_id` → `users(id)`
|
||||
|
||||
### accounts
|
||||
| Column Name | Data Type | Nullable | Default | Comment |
|
||||
| ----------- | --------- | -------- | ------- | ------- |
|
||||
| id | BIGINT UNSIGNED | NOT NULL | AUTO_INCREMENT | |
|
||||
| shop_id | BIGINT UNSIGNED | NOT NULL | | |
|
||||
| user_id | BIGINT UNSIGNED | NOT NULL | | |
|
||||
| name | VARCHAR(64) | NOT NULL | | |
|
||||
| type | ENUM('cash','bank','alipay','wechat','other') | NOT NULL | cash | |
|
||||
| balance | DECIMAL(18,2) | NOT NULL | 0.00 | |
|
||||
| status | TINYINT UNSIGNED | NOT NULL | 1 | |
|
||||
| created_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | |
|
||||
| updated_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | |
|
||||
| deleted_at | DATETIME | YES | | |
|
||||
|
||||
**Indexes**: - PRIMARY KEY: `id` - KEY: `idx_accounts_shop` (`shop_id`) - UNIQUE: `ux_accounts_shop_name` (`shop_id`,`name`)
|
||||
**Foreign Keys**: - `fk_accounts_shop`: `shop_id` → `shops(id)` - `fk_accounts_user`: `user_id` → `users(id)`
|
||||
|
||||
### sales_orders
|
||||
| Column Name | Data Type | Nullable | Default | Comment |
|
||||
| ----------- | --------- | -------- | ------- | ------- |
|
||||
| id | BIGINT UNSIGNED | NOT NULL | AUTO_INCREMENT | |
|
||||
| shop_id | BIGINT UNSIGNED | NOT NULL | | |
|
||||
| user_id | BIGINT UNSIGNED | NOT NULL | | 创建人 |
|
||||
| customer_id | BIGINT UNSIGNED | YES | | |
|
||||
| order_no | VARCHAR(32) | NOT NULL | | |
|
||||
| order_time | DATETIME | NOT NULL | | |
|
||||
| status | ENUM('draft','approved','returned','void') | NOT NULL | draft | |
|
||||
| amount | DECIMAL(18,2) | NOT NULL | 0.00 | 应收合计 |
|
||||
| paid_amount | DECIMAL(18,2) | NOT NULL | 0.00 | 已收合计 |
|
||||
| remark | VARCHAR(255) | YES | | |
|
||||
| created_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | |
|
||||
| updated_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | |
|
||||
| deleted_at | DATETIME | YES | | |
|
||||
|
||||
**Indexes**: - PRIMARY KEY: `id` - KEY: `idx_sales_shop_time` (`shop_id`,`order_time`) - KEY: `idx_sales_customer` (`customer_id`) - UNIQUE: `ux_sales_order_no` (`shop_id`,`order_no`)
|
||||
**Foreign Keys**: - `fk_sales_shop`: `shop_id` → `shops(id)` - `fk_sales_user`: `user_id` → `users(id)` - `fk_sales_customer`: `customer_id` → `customers(id)`
|
||||
|
||||
### sales_order_items
|
||||
| Column Name | Data Type | Nullable | Default | Comment |
|
||||
| ----------- | --------- | -------- | ------- | ------- |
|
||||
| id | BIGINT UNSIGNED | NOT NULL | AUTO_INCREMENT | |
|
||||
| order_id | BIGINT UNSIGNED | NOT NULL | | |
|
||||
| product_id | BIGINT UNSIGNED | NOT NULL | | |
|
||||
| quantity | DECIMAL(18,3) | NOT NULL | | |
|
||||
| unit_price | DECIMAL(18,2) | NOT NULL | | |
|
||||
| discount_rate | DECIMAL(5,2) | NOT NULL | 0.00 | 折扣百分比0-100 |
|
||||
| amount | DECIMAL(18,2) | NOT NULL | | |
|
||||
|
||||
**Indexes**: - PRIMARY KEY: `id` - KEY: `idx_soi_order` (`order_id`) - KEY: `idx_soi_product` (`product_id`)
|
||||
**Foreign Keys**: - `fk_soi_order`: `order_id` → `sales_orders(id)` ON DELETE CASCADE - `fk_soi_product`: `product_id` → `products(id)`
|
||||
|
||||
### purchase_orders
|
||||
| Column Name | Data Type | Nullable | Default | Comment |
|
||||
| ----------- | --------- | -------- | ------- | ------- |
|
||||
| id | BIGINT UNSIGNED | NOT NULL | AUTO_INCREMENT | |
|
||||
| shop_id | BIGINT UNSIGNED | NOT NULL | | |
|
||||
| user_id | BIGINT UNSIGNED | NOT NULL | | |
|
||||
| supplier_id | BIGINT UNSIGNED | YES | | |
|
||||
| order_no | VARCHAR(32) | NOT NULL | | |
|
||||
| order_time | DATETIME | NOT NULL | | |
|
||||
| status | ENUM('draft','approved','void') | NOT NULL | draft | |
|
||||
| amount | DECIMAL(18,2) | NOT NULL | 0.00 | 应付合计 |
|
||||
| paid_amount | DECIMAL(18,2) | NOT NULL | 0.00 | 已付合计 |
|
||||
| remark | VARCHAR(255) | YES | | |
|
||||
| created_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | |
|
||||
| updated_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | |
|
||||
| deleted_at | DATETIME | YES | | |
|
||||
|
||||
**Indexes**: - PRIMARY KEY: `id` - KEY: `idx_purchase_shop_time` (`shop_id`,`order_time`) - KEY: `idx_purchase_supplier` (`supplier_id`) - UNIQUE: `ux_purchase_order_no` (`shop_id`,`order_no`)
|
||||
**Foreign Keys**: - `fk_purchase_shop`: `shop_id` → `shops(id)` - `fk_purchase_user`: `user_id` → `users(id)` - `fk_purchase_supplier`: `supplier_id` → `suppliers(id)`
|
||||
|
||||
### purchase_order_items
|
||||
| Column Name | Data Type | Nullable | Default | Comment |
|
||||
| ----------- | --------- | -------- | ------- | ------- |
|
||||
| id | BIGINT UNSIGNED | NOT NULL | AUTO_INCREMENT | |
|
||||
| order_id | BIGINT UNSIGNED | NOT NULL | | |
|
||||
| product_id | BIGINT UNSIGNED | NOT NULL | | |
|
||||
| quantity | DECIMAL(18,3) | NOT NULL | | |
|
||||
| unit_price | DECIMAL(18,2) | NOT NULL | | |
|
||||
| amount | DECIMAL(18,2) | NOT NULL | | |
|
||||
|
||||
**Indexes**: - PRIMARY KEY: `id` - KEY: `idx_poi_order` (`order_id`) - KEY: `idx_poi_product` (`product_id`)
|
||||
**Foreign Keys**: - `fk_poi_order`: `order_id` → `purchase_orders(id)` ON DELETE CASCADE - `fk_poi_product`: `product_id` → `products(id)`
|
||||
|
||||
### payments
|
||||
| Column Name | Data Type | Nullable | Default | Comment |
|
||||
| ----------- | --------- | -------- | ------- | ------- |
|
||||
| id | BIGINT UNSIGNED | NOT NULL | AUTO_INCREMENT | |
|
||||
| shop_id | BIGINT UNSIGNED | NOT NULL | | |
|
||||
| user_id | BIGINT UNSIGNED | NOT NULL | | |
|
||||
| biz_type | ENUM('sale','purchase','other') | NOT NULL | | |
|
||||
| biz_id | BIGINT UNSIGNED | YES | | 业务表ID:sales_orders/purchase_orders/other_transactions |
|
||||
| account_id | BIGINT UNSIGNED | NOT NULL | | |
|
||||
| direction | ENUM('in','out') | NOT NULL | | 收款/付款 |
|
||||
| amount | DECIMAL(18,2) | NOT NULL | | |
|
||||
| pay_time | DATETIME | NOT NULL | | |
|
||||
| remark | VARCHAR(255) | YES | | |
|
||||
| created_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | |
|
||||
|
||||
**Indexes**: - PRIMARY KEY: `id` - KEY: `idx_payments_shop_time` (`shop_id`,`pay_time`) - KEY: `idx_payments_biz` (`biz_type`,`biz_id`)
|
||||
**Foreign Keys**: - `fk_payments_shop`: `shop_id` → `shops(id)` - `fk_payments_user`: `user_id` → `users(id)` - `fk_payments_account`: `account_id` → `accounts(id)`
|
||||
|
||||
### other_transactions
|
||||
| Column Name | Data Type | Nullable | Default | Comment |
|
||||
| ----------- | --------- | -------- | ------- | ------- |
|
||||
| id | BIGINT UNSIGNED | NOT NULL | AUTO_INCREMENT | |
|
||||
| shop_id | BIGINT UNSIGNED | NOT NULL | | |
|
||||
| user_id | BIGINT UNSIGNED | NOT NULL | | |
|
||||
| type | ENUM('income','expense') | NOT NULL | | |
|
||||
| category | VARCHAR(64) | NOT NULL | | |
|
||||
| counterparty_type | VARCHAR(32) | YES | | customer/supplier/other |
|
||||
| counterparty_id | BIGINT UNSIGNED | YES | | |
|
||||
| account_id | BIGINT UNSIGNED | NOT NULL | | |
|
||||
| amount | DECIMAL(18,2) | NOT NULL | | |
|
||||
| tx_time | DATETIME | NOT NULL | | |
|
||||
| remark | VARCHAR(255) | YES | | |
|
||||
| created_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | |
|
||||
| updated_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | |
|
||||
| deleted_at | DATETIME | YES | | |
|
||||
|
||||
**Indexes**: - PRIMARY KEY: `id` - KEY: `idx_ot_shop_time` (`shop_id`,`tx_time`) - KEY: `idx_ot_account` (`account_id`)
|
||||
**Foreign Keys**: - `fk_ot_shop`: `shop_id` → `shops(id)` - `fk_ot_user`: `user_id` → `users(id)` - `fk_ot_account`: `account_id` → `accounts(id)`
|
||||
|
||||
### 触发器
|
||||
- `trg_products_bi`: BEFORE INSERT ON `products` → 设置 `products.search_text`
|
||||
- `trg_products_au`: BEFORE UPDATE ON `products` → 维护 `products.search_text`
|
||||
- `trg_palias_ai`: AFTER INSERT ON `product_aliases` → 重建 `products.search_text`
|
||||
- `trg_palias_au`: AFTER UPDATE ON `product_aliases` → 重建 `products.search_text`
|
||||
- `trg_palias_ad`: AFTER DELETE ON `product_aliases` → 重建 `products.search_text`
|
||||
|
||||
|
||||
### notices
|
||||
| Column Name | Data Type | Nullable | Default | Comment |
|
||||
| ----------- | --------- | -------- | ------- | ------- |
|
||||
| id | BIGINT UNSIGNED | NOT NULL | AUTO_INCREMENT | |
|
||||
| shop_id | BIGINT UNSIGNED | NOT NULL | | |
|
||||
| user_id | BIGINT UNSIGNED | NOT NULL | | |
|
||||
| title | VARCHAR(120) | NOT NULL | | |
|
||||
| content | VARCHAR(500) | NOT NULL | | |
|
||||
| tag | VARCHAR(32) | YES | | |
|
||||
| is_pinned | TINYINT(1) | NOT NULL | 0 | |
|
||||
| starts_at | DATETIME | YES | | |
|
||||
| ends_at | DATETIME | YES | | |
|
||||
| status | ENUM('draft','published','offline') | NOT NULL | published | |
|
||||
| created_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | |
|
||||
| updated_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | |
|
||||
| deleted_at | DATETIME | YES | | |
|
||||
|
||||
**Indexes**: - PRIMARY KEY: `id` - KEY: `idx_notices_shop` (`shop_id`,`status`,`is_pinned`,`created_at`) - KEY: `idx_notices_time` (`starts_at`,`ends_at`)
|
||||
**Foreign Keys**: - `fk_notices_shop`: `shop_id` → `shops(id)` ON UPDATE NO ACTION ON DELETE NO ACTION - `fk_notices_user`: `user_id` → `users(id)` ON UPDATE NO ACTION ON DELETE NO ACTION
|
||||
|
||||
### 附:演示种子数据(非完整,仅用于联调验证)
|
||||
- 演示店铺:演示店A(用户 3,全部店长 owner)
|
||||
- 商品域:基础单位3条、类别2条、全局SKU2条、商品2条(含别名/价格/库存/图片)
|
||||
- 往来与账户:客户2、供应商2、账户3
|
||||
- 单据:销售单1(含明细2)与进货单1(含明细2)、收付款各1、其他收支2
|
||||
- 审核与公告:part_submissions 1、attachments 1、notices 2、新增 wechat 身份与会话各1
|
||||
|
||||
658
doc/openapi.yaml
Normal file
658
doc/openapi.yaml
Normal file
@@ -0,0 +1,658 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: PartsInquiry API
|
||||
version: 0.1.0
|
||||
description: >-
|
||||
所有接口定义集中于此文件。每个 path 在 summary/description 中标注实现状态。
|
||||
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)
|
||||
description: 返回当前店铺可见的公告列表。后端与前端均已接入。
|
||||
parameters:
|
||||
- in: header
|
||||
name: X-Shop-Id
|
||||
required: false
|
||||
schema:
|
||||
type: integer
|
||||
description: 店铺ID,缺省为 1
|
||||
responses:
|
||||
'200':
|
||||
description: 成功
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Notice'
|
||||
/api/metrics/overview:
|
||||
get:
|
||||
summary: 概览统计(❌ Partially Implemented)
|
||||
description: 返回今日/本月销售额、本月利润与库存商品数量。前端已接入,后端待实现。
|
||||
responses:
|
||||
'200':
|
||||
description: 成功
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/MetricsOverview'
|
||||
/api/accounts:
|
||||
get:
|
||||
summary: 账户列表(❌ Partially Implemented)
|
||||
description: 前端账户选择页已接入,后端返回数组或 {list:[]} 皆可。
|
||||
responses:
|
||||
'200':
|
||||
description: 成功
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
oneOf:
|
||||
- type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Account'
|
||||
- type: object
|
||||
properties:
|
||||
list:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Account'
|
||||
/api/suppliers:
|
||||
get:
|
||||
summary: 供应商搜索(❌ Partially Implemented)
|
||||
parameters:
|
||||
- in: query
|
||||
name: kw
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: 成功
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
oneOf:
|
||||
- type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Supplier'
|
||||
- type: object
|
||||
properties:
|
||||
list:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Supplier'
|
||||
/api/other-transactions:
|
||||
post:
|
||||
summary: 新建其他收入/支出(❌ Partially Implemented)
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/CreateOtherTransactionRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: 成功
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
format: int64
|
||||
/api/products:
|
||||
get:
|
||||
summary: 商品搜索(✅ Fully Implemented)
|
||||
description: 支持 kw/page/size/categoryId;返回 {list:[]} 以兼容前端。
|
||||
parameters:
|
||||
- in: query
|
||||
name: kw
|
||||
schema:
|
||||
type: string
|
||||
- in: query
|
||||
name: page
|
||||
schema:
|
||||
type: integer
|
||||
default: 1
|
||||
- in: query
|
||||
name: size
|
||||
schema:
|
||||
type: integer
|
||||
default: 50
|
||||
responses:
|
||||
'200':
|
||||
description: 成功
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
oneOf:
|
||||
- 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:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Product'
|
||||
/api/customers:
|
||||
get:
|
||||
summary: 客户搜索(❌ Partially Implemented)
|
||||
description: 前端已接入查询参数 kw/page/size,后端待实现或对齐。
|
||||
parameters:
|
||||
- in: query
|
||||
name: kw
|
||||
schema:
|
||||
type: string
|
||||
- in: query
|
||||
name: page
|
||||
schema:
|
||||
type: integer
|
||||
default: 1
|
||||
- in: query
|
||||
name: size
|
||||
schema:
|
||||
type: integer
|
||||
default: 50
|
||||
responses:
|
||||
'200':
|
||||
description: 成功
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
oneOf:
|
||||
- type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Customer'
|
||||
- type: object
|
||||
properties:
|
||||
list:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Customer'
|
||||
/api/orders:
|
||||
post:
|
||||
summary: 新建单据(❌ Partially Implemented)
|
||||
description: 前端开单页已提交 payload,后端待实现。
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/CreateOrderRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: 成功
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
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:
|
||||
id:
|
||||
type: integer
|
||||
format: int64
|
||||
shopId:
|
||||
type: integer
|
||||
format: int64
|
||||
userId:
|
||||
type: integer
|
||||
format: int64
|
||||
title:
|
||||
type: string
|
||||
content:
|
||||
type: string
|
||||
tag:
|
||||
type: string
|
||||
pinned:
|
||||
type: boolean
|
||||
startsAt:
|
||||
type: string
|
||||
format: date-time
|
||||
MetricsOverview:
|
||||
type: object
|
||||
properties:
|
||||
todaySales:
|
||||
type: string
|
||||
example: '1234.56'
|
||||
monthSales:
|
||||
type: string
|
||||
example: '23456.78'
|
||||
monthProfit:
|
||||
type: string
|
||||
example: '3456.78'
|
||||
stockCount:
|
||||
type: string
|
||||
example: '1200'
|
||||
Account:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
format: int64
|
||||
name:
|
||||
type: string
|
||||
type:
|
||||
type: string
|
||||
enum: [cash, bank, alipay, wechat, other]
|
||||
balance:
|
||||
type: number
|
||||
Supplier:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
format: int64
|
||||
name:
|
||||
type: string
|
||||
mobile:
|
||||
type: string
|
||||
CreateOtherTransactionRequest:
|
||||
type: object
|
||||
properties:
|
||||
type:
|
||||
type: string
|
||||
enum: [income, expense]
|
||||
category:
|
||||
type: string
|
||||
counterpartyId:
|
||||
type: integer
|
||||
format: int64
|
||||
nullable: true
|
||||
accountId:
|
||||
type: integer
|
||||
format: int64
|
||||
amount:
|
||||
type: number
|
||||
txTime:
|
||||
type: string
|
||||
format: date
|
||||
remark:
|
||||
type: string
|
||||
endsAt:
|
||||
type: string
|
||||
format: date-time
|
||||
status:
|
||||
type: string
|
||||
enum: [DRAFT, PUBLISHED, OFFLINE]
|
||||
createdAt:
|
||||
type: string
|
||||
format: date-time
|
||||
updatedAt:
|
||||
type: string
|
||||
format: date-time
|
||||
Product:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
format: int64
|
||||
code:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
price:
|
||||
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:
|
||||
id:
|
||||
type: integer
|
||||
format: int64
|
||||
name:
|
||||
type: string
|
||||
mobile:
|
||||
type: string
|
||||
CreateOrderRequest:
|
||||
type: object
|
||||
properties:
|
||||
type:
|
||||
type: string
|
||||
description: 'sale.out/sale.return/sale.collect/purchase/income/expense 等'
|
||||
orderTime:
|
||||
type: string
|
||||
format: date
|
||||
customerId:
|
||||
type: integer
|
||||
format: int64
|
||||
nullable: true
|
||||
items:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
productId:
|
||||
type: integer
|
||||
format: int64
|
||||
quantity:
|
||||
type: number
|
||||
unitPrice:
|
||||
type: number
|
||||
amount:
|
||||
type: number
|
||||
|
||||
167
doc/requirements.md
Normal file
167
doc/requirements.md
Normal file
@@ -0,0 +1,167 @@
|
||||
* ### **配件查询App需求规格说明书**
|
||||
|
||||
#### 1.0 项目概述
|
||||
本项目旨在开发一款面向小微商户的移动端进销存管理应用,命名为“配件查询”。该应用核心功能是帮助用户高效管理商品、库存、销售、采购、客户、供应商及财务收支,并通过数据报表提供经营状况分析,助力商户实现数字化经营。
|
||||
参考的小程序“智慧记进销存”,但是多了一个配件查询功能,以下所罗列的内容大多也参考至该小程序,如有歧义可优先参照这个小程序,拿不准优先问。
|
||||
|
||||
#### 2.0 功能模块需求
|
||||
|
||||
**2.1 首页 (Dashboard)**
|
||||
|
||||
* **2.1.1 核心数据概览:** 首页需直观展示当日、当月的核心经营数据。
|
||||
|
||||
* 今日销售额
|
||||
* 本月销售额
|
||||
* 本月利润
|
||||
* 库存商品数量
|
||||
|
||||
**2.1.2 广告位:** 在首页区域提供一个展示广告的区域。
|
||||
|
||||
* **2.1.3 快捷功能入口:** 提供一个快捷功能区域,方便用户快速访问常用功能。
|
||||
|
||||
* 默认应包含:客户管理、销售开单、账户管理、供应商管理、进货开单、其他支出、VIP会员、报表统计等。
|
||||
|
||||
* **2.1.4 在线客服:** 提供一个悬浮的“咨询”或“在线客服”入口,方便用户随时获取帮助。
|
||||
|
||||
**2.2 货品管理模块**
|
||||
|
||||
* **2.2.1 货品列表与库存:**
|
||||
* 展示所有货品的列表,包含名称、库存等基本信息。
|
||||
* 支持按“全部类别”或指定类别筛选货品。
|
||||
* 提供搜索功能,支持通过货品名称或条形码进行模糊查找。
|
||||
* 列表底部显示总货品种类数量。
|
||||
* 当库存为空时,应有明显的空状态提示,并引导用户“点击右上角‘+’录入货品信息”。
|
||||
* **2.2.2 新增/编辑货品:**
|
||||
* 支持添加商品图片。
|
||||
* App端支持录入或扫描商品条形码;小程序端仅支持手动录入,不支持扫码。
|
||||
* **货品名称**为必填项。
|
||||
* 可为货品选择**类别**和**主单位**。
|
||||
* 支持自定义扩展货品属性(如品牌、型号、产地、保质期等)。
|
||||
* 货品图片支持多图上传,支持拖拽排序,支持图片放大预览。
|
||||
* 可录入**当前库存**、**安全库存**(一个数值区间,用于库存预警)。
|
||||
* 需分别录入四种价格,**进货价**、**批发价**、**大单报价**和**零售价**。
|
||||
* 提供**备注**字段,用于记录额外信息。
|
||||
* 保存后,可选择“继续新增”或返回列表。
|
||||
* **2.2.3 货品设置:**
|
||||
* 支持自定义**货品类别**管理。
|
||||
* 支持自定义**货品单位**管理。
|
||||
* 提供开关选项,允许用户选择是否“隐藏零库存商品”和“隐藏进货价”。
|
||||
|
||||
**2.3 开单(交易)模块**
|
||||
* **2.3.1 核心功能:** 该模块是应用的核心操作区,整合了销售、进货和财务记账功能。
|
||||
* **2.3.2 销售开单:**
|
||||
* **出货单:**
|
||||
* 自动记录开单**时间**,并支持手动修改。
|
||||
* 可选择**客户**,默认为“零售客户”。
|
||||
* 通过“+”号从货品列表中选择商品,自动计算**合计金额**。
|
||||
* 支持在订单中对单个商品进行操作(如修改数量、价格等)。
|
||||
* **退货单:** 用于处理客户退货业务。
|
||||
* **收款单:** 用于处理销售单的后续收款或直接创建收款记录。
|
||||
* **2.3.3 进货开单:** 用于记录从供应商处采购商品的业务流程。
|
||||
* **2.3.4 其他收入/支出:**
|
||||
* **其他收入:**
|
||||
* 支持对收入进行分类,如“销售收入”、“经营所得”、“利息收入”等。
|
||||
* 可选择**往来单位**和**结算账户**(如现金、银行等)。
|
||||
* 可添加备注并选择日期。
|
||||
* **其他支出:**
|
||||
* 支持对支出进行分类,如“经营支出”、“办公用品”、“房租”等。
|
||||
* 同样支持选择**往来单位**和**结算账户**。
|
||||
|
||||
**2.4 明细查询模块**
|
||||
* **2.4.1 维度筛选:**
|
||||
* 提供按时间维度(自定义、本周、今日、本月、本年)快速筛选单据。
|
||||
* 提供按业务类型(销售、进货、收银、资金、盘点)进行分类查看。
|
||||
* **2.4.2 单据列表:**
|
||||
* 在选定维度下,以列表形式展示所有相关单据。
|
||||
* 提供搜索功能,支持通过单据号、客户/供应商名、品名、备注等关键字查询。
|
||||
* 显示当前筛选条件下的总金额。
|
||||
* 当无数据时,提供清晰的空状态提示。
|
||||
* 提供“+”号,支持在当前分类下快速新建单据。
|
||||
|
||||
**2.5 报表统计模块**
|
||||
* **2.5.1 资金报表:**
|
||||
* **利润统计:** 分析指定时间范围内的收入、支出和利润。
|
||||
* **营业员统计:** 按销售人员维度统计销售业绩。
|
||||
* **经营业绩:** 提供综合性的经营状况分析。
|
||||
* **导入导出模块:** 提供导入导出功能方便用户切换手机或账号后仍能将旧数据导入。
|
||||
* **2.5.2 进销存报表:**
|
||||
* **销售统计:** 按商品、客户、时间等维度分析销售数据。
|
||||
* **进货统计:** 按商品、供应商、时间等维度分析采购数据。
|
||||
* **库存统计:** 提供当前库存成本、数量及分布情况的报告。
|
||||
* **应收/应付对账单:** 生成与客户和供应商的对账单据。
|
||||
|
||||
**2.6 “我的”(用户中心)模块**
|
||||
* **2.6.1 用户信息:** 显示用户头像、店铺名称、注册手机号及“老板”身份标识。
|
||||
* **2.6.2 会员与订单:**
|
||||
* 提供**VIP会员**入口,展示会员特权。
|
||||
* 提供**我的订单**入口,可能用于查看应用内服务订单。
|
||||
* **2.6.3 基础管理:**
|
||||
* **供应商管理**
|
||||
* **客户管理**
|
||||
* **客户报价**
|
||||
* **店铺管理**
|
||||
* **2.6.4 设置中心:**
|
||||
* **账号与安全:**
|
||||
* 修改个人信息(头像、姓名)。
|
||||
* 修改登录密码。
|
||||
* **商品设置:**
|
||||
* **系统参数:**
|
||||
* 提供多种业务逻辑开关,如:“销售价低于进货价时提示”、“销售单默认全部收款”、“启用单行折扣”、“启用客户/供应商双身份”。
|
||||
* **关于与协议:** 包含“关于我们”、“隐私协议”、“个人信息安全投诉”等静态页面。
|
||||
* **账号操作:** 提供“账号注销”和“退出登录”功能。
|
||||
|
||||
#### 3.0 全局性需求
|
||||
|
||||
* **3.1 导航:** 采用底部Tab栏导航,包含“首页”、“货品”、“开单”、“明细”、“我的”五个主要模块。
|
||||
* **3.2 统一的UI/UX:** 应用整体风格简洁、清晰,操作流程符合移动端使用习惯。
|
||||
* **3.3 空状态页面:** 在列表、报表等数据为空的页面,需提供友好的空状态提示图和引导性文字。
|
||||
* **3.4 数据同步:** 应用数据应在云端同步,保证用户更换设备或多设备使用时数据一致性。
|
||||
* **3.5 多租户数据隔离:** 所有业务数据按店铺(租户)隔离,用户不可访问他人数据。
|
||||
* 所有业务表需包含`user_id`并在读取/写入中强制按`user_id`过滤。
|
||||
* 支持租户内角色与权限控制;导出仅限本租户数据。
|
||||
* **3.6 公共SKU(全局商品库)众包与审核:** 全体用户共同补充、纠错SKU,经审核发布为全局可选SKU。
|
||||
* 用户可提交“新增SKU/编辑建议”,进入审核流(草稿/待审/驳回/发布/下架)。
|
||||
* 全局SKU字段:名称、品牌、规格、条码、主单位、图片、别名、分类标签等。
|
||||
* 各用户通过“本地商品”引用全局SKU,并保留本地私有字段(价格、库存、分类、单位换算、条码别名等)。
|
||||
* **3.7 商品模糊查询(增强):** 在货品列表、开单选品、对账等场景支持多字段模糊匹配。
|
||||
* 支持名称/条码/别名/拼音/品牌/规格模糊匹配,并高亮命中片段。
|
||||
* 支持全局SKU与本地商品联合检索,优先展示本地商品;结果可分页。
|
||||
* 需满足大规模SKU下的性能目标;可通过系统参数配置匹配策略。
|
||||
|
||||
* **3.8 客户端平台:** 提供移动App与小程序;小程序不支持商品条形码扫描功能。
|
||||
|
||||
* **3.9 多列销售价格:** 销售价格分四列,即同一种商品有四个销售价格
|
||||
|
||||
### 配件查询
|
||||
|
||||
1. **数据查询功能**
|
||||
- 多参数组合查询(分类、尺寸、型号等)
|
||||
- 模糊匹配关键字
|
||||
- 分页展示查询结果
|
||||
- 一键导出Excel数据
|
||||
2. **数据提交系统**
|
||||
- 用户提交新配件数据
|
||||
- 型号为唯一必填项
|
||||
- 支持图片上传
|
||||
- 提交后等待管理员审核
|
||||
3. **审核管理系统**
|
||||
- 管理员查看待审核列表
|
||||
- 可编辑所有字段
|
||||
- 支持图片更新和删除
|
||||
- 一键批准或拒绝提交
|
||||
4. **图片管理系统**
|
||||
- 每条数据可关联多张图片
|
||||
- 点击图片可放大查看
|
||||
- 管理员可管理所有图片
|
||||
- 自动处理文件名冲突
|
||||
|
||||
## 全局说明(必看)
|
||||
|
||||
由于这个文档写的还不是很完善,目前有存疑的部分先行参考小程序小程序“智慧记进销存”(功能和按钮可以参考,界面样式除外),管理端文档目前待定。
|
||||
|
||||
客户要求的是做双端应用(app端+小程序端),需要考虑兼容性相关问题。
|
||||
|
||||
本程序和“智慧记进销存”大多一致,主要的区别在于客户有配件查询要求,即在产品页面中要额外加一个配件查询按钮或入口,且要求一个产品要有四个销售价格(先按零售价 分销价 批发价 大客户价),且要求能自定义添加各种规格(尺寸,孔径等)。
|
||||
|
||||
有疑惑的部分一定要及时沟通(如未提及的页面和功能需要确认的时候)
|
||||
|
||||
31
frontend/common/config.js
Normal file
31
frontend/common/config.js
Normal file
@@ -0,0 +1,31 @@
|
||||
// 统一配置:禁止在业务代码中硬编码
|
||||
// 优先级:环境变量(Vite/HBuilderX 构建注入) > 本地存储 > 默认值
|
||||
|
||||
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://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);
|
||||
|
||||
|
||||
// 默认用户(可移除):
|
||||
// - 用途:开发/演示环境,自动将用户固定为“张老板”(id=2)
|
||||
// - 开关优先级:环境变量 > 本地存储 > 默认值
|
||||
// - 生产默认关闭(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 || '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') || '') : '';
|
||||
export const DEFAULT_USER_ID = Number(envDefaultUserId || storageDefaultUserId || 2);
|
||||
|
||||
|
||||
19
frontend/common/constants.js
Normal file
19
frontend/common/constants.js
Normal file
@@ -0,0 +1,19 @@
|
||||
// 统一常量配置:其他收入/支出分类,禁止在业务中硬编码
|
||||
export const INCOME_CATEGORIES = [
|
||||
{ key: 'sale_income', label: '销售收入' },
|
||||
{ key: 'operation_income', label: '经营所得' },
|
||||
{ key: 'interest_income', label: '利息收入' },
|
||||
{ key: 'investment_income', label: '投资收入' },
|
||||
{ key: 'other_income', label: '其它收入' }
|
||||
]
|
||||
|
||||
export const EXPENSE_CATEGORIES = [
|
||||
{ key: 'operation_expense', label: '经营支出' },
|
||||
{ key: 'office_supplies', label: '办公用品' },
|
||||
{ key: 'rent', label: '房租' },
|
||||
{ key: 'interest_expense', label: '利息支出' },
|
||||
{ key: 'other_expense', label: '其它支出' }
|
||||
]
|
||||
|
||||
|
||||
|
||||
96
frontend/common/http.js
Normal file
96
frontend/common/http.js
Normal file
@@ -0,0 +1,96 @@
|
||||
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
|
||||
if (path.startsWith('http')) return 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
|
||||
const options = { url: buildUrl(path), method: 'GET', data: params, header: headers }
|
||||
requestWithFallback(options, API_BASE_URL_CANDIDATES, 0, resolve, reject)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
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
|
||||
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)
|
||||
})
|
||||
}
|
||||
|
||||
164
frontend/components/ImageUploader.vue
Normal file
164
frontend/components/ImageUploader.vue
Normal 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>
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -5,6 +5,66 @@
|
||||
"style": {
|
||||
"navigationBarTitleText": "五金配件管家"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/order/create",
|
||||
"style": {
|
||||
"navigationBarTitleText": "开单"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/product/select",
|
||||
"style": {
|
||||
"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": {
|
||||
"navigationBarTitleText": "选择客户"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/supplier/select",
|
||||
"style": {
|
||||
"navigationBarTitleText": "选择供应商"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/account/select",
|
||||
"style": {
|
||||
"navigationBarTitleText": "选择账户"
|
||||
}
|
||||
}
|
||||
],
|
||||
"globalStyle": {
|
||||
|
||||
46
frontend/pages/account/select.vue
Normal file
46
frontend/pages/account/select.vue
Normal file
@@ -0,0 +1,46 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<scroll-view scroll-y class="list">
|
||||
<view class="item" v-for="a in accounts" :key="a.id" @click="select(a)">
|
||||
<view class="name">{{ a.name }}</view>
|
||||
<view class="meta">{{ typeLabel(a.type) }} · 余额:{{ a.balance?.toFixed ? a.balance.toFixed(2) : a.balance }}</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { get } from '../../common/http.js'
|
||||
const TYPE_MAP = { cash: '现金', bank: '银行', alipay: '支付宝', wechat: '微信', other: '其他' }
|
||||
export default {
|
||||
data() { return { accounts: [] } },
|
||||
async onLoad() {
|
||||
try {
|
||||
const res = await get('/api/accounts')
|
||||
this.accounts = Array.isArray(res) ? res : (res?.list || [])
|
||||
} catch(e) { uni.showToast({ title: '加载失败', icon: 'none' }) }
|
||||
},
|
||||
methods: {
|
||||
select(a) {
|
||||
const opener = getCurrentPages()[getCurrentPages().length-2]
|
||||
if (opener && opener.$vm) {
|
||||
opener.$vm.selectedAccountId = a.id
|
||||
opener.$vm.selectedAccountName = a.name
|
||||
}
|
||||
uni.navigateBack()
|
||||
},
|
||||
typeLabel(t) { return TYPE_MAP[t] || t }
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.page { display:flex; flex-direction: column; height: 100vh; }
|
||||
.list { flex:1; }
|
||||
.item { padding: 20rpx 24rpx; background:#fff; border-bottom: 1rpx solid #f1f1f1; }
|
||||
.name { color:#333; margin-bottom: 6rpx; }
|
||||
.meta { color:#888; font-size: 24rpx; }
|
||||
</style>
|
||||
|
||||
|
||||
|
||||
51
frontend/pages/customer/select.vue
Normal file
51
frontend/pages/customer/select.vue
Normal file
@@ -0,0 +1,51 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<view class="search">
|
||||
<input v-model="kw" placeholder="搜索客户名称/电话" @confirm="search" />
|
||||
<button size="mini" @click="search">搜索</button>
|
||||
</view>
|
||||
<scroll-view scroll-y class="list">
|
||||
<view class="item" v-for="c in customers" :key="c.id" @click="select(c)">
|
||||
<view class="name">{{ c.name }}</view>
|
||||
<view class="meta">{{ c.mobile || '—' }}</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { get } from '../../common/http.js'
|
||||
export default {
|
||||
data() { return { kw: '', customers: [] } },
|
||||
onLoad() { this.search() },
|
||||
methods: {
|
||||
async search() {
|
||||
try {
|
||||
const res = await get('/api/customers', { kw: this.kw, page: 1, size: 50 })
|
||||
this.customers = Array.isArray(res?.list) ? res.list : (Array.isArray(res) ? res : [])
|
||||
} catch(e) { uni.showToast({ title: '加载失败', icon: 'none' }) }
|
||||
},
|
||||
select(c) {
|
||||
const opener = getCurrentPages()[getCurrentPages().length-2]
|
||||
if (opener && opener.$vm) {
|
||||
opener.$vm.order.customerId = c.id
|
||||
opener.$vm.customerName = c.name
|
||||
}
|
||||
uni.navigateBack()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.page { display:flex; flex-direction: column; height: 100vh; }
|
||||
.search { display:flex; gap: 12rpx; padding: 16rpx; background:#fff; }
|
||||
.search input { flex:1; background:#f6f6f6; border-radius: 12rpx; padding: 12rpx; }
|
||||
.list { flex:1; }
|
||||
.item { padding: 20rpx 24rpx; background:#fff; border-bottom: 1rpx solid #f1f1f1; }
|
||||
.name { color:#333; margin-bottom: 6rpx; }
|
||||
.meta { color:#888; font-size: 24rpx; }
|
||||
</style>
|
||||
|
||||
|
||||
|
||||
@@ -12,23 +12,30 @@
|
||||
<view class="kpi">
|
||||
<view class="kpi-item">
|
||||
<text class="kpi-label">今日销售额</text>
|
||||
<text class="kpi-value">{{ todayAmount }}</text>
|
||||
<text class="kpi-value">{{ kpi.todaySales }}</text>
|
||||
</view>
|
||||
<view class="kpi-item">
|
||||
<text class="kpi-label">本月销售额</text>
|
||||
<text class="kpi-value">{{ kpi.monthSales }}</text>
|
||||
</view>
|
||||
<view class="kpi-item">
|
||||
<text class="kpi-label">本月利润</text>
|
||||
<text class="kpi-value">{{ monthProfit }}</text>
|
||||
<text class="kpi-value">{{ kpi.monthProfit }}</text>
|
||||
</view>
|
||||
<view class="kpi-item">
|
||||
<text class="kpi-label">库存数量</text>
|
||||
<text class="kpi-value">{{ stockQty }}</text>
|
||||
<text class="kpi-label">库存商品数量</text>
|
||||
<text class="kpi-value">{{ kpi.stockCount }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 公告栏:自动轮播,可点击查看详情 -->
|
||||
<!-- 广告栏:自动轮播,可点击查看详情 -->
|
||||
<view class="notice">
|
||||
<view class="notice-left">公告</view>
|
||||
<swiper class="notice-swiper" circular autoplay interval="4000" duration="400" vertical>
|
||||
<view class="notice-left">广告</view>
|
||||
<view v-if="loadingNotices" class="notice-swiper" style="display:flex;align-items:center;color:#6b5a2a;">加载中...</view>
|
||||
<view v-else-if="noticeError" class="notice-swiper" style="display:flex;align-items:center;color:#dd524d;">{{ noticeError }}</view>
|
||||
<view v-else-if="!notices.length" class="notice-swiper" style="display:flex;align-items:center;color:#6b5a2a;">暂无公告</view>
|
||||
<swiper v-else class="notice-swiper" circular autoplay interval="4000" duration="400" vertical>
|
||||
<swiper-item v-for="(n, idx) in notices" :key="idx">
|
||||
<view class="notice-item" @click="onNoticeTap(n)">
|
||||
<text class="notice-text">{{ n.text }}</text>
|
||||
@@ -36,7 +43,6 @@
|
||||
</view>
|
||||
</swiper-item>
|
||||
</swiper>
|
||||
<view class="notice-right" @click="onNoticeList">更多</view>
|
||||
</view>
|
||||
|
||||
<!-- 分割标题:产品与功能 -->
|
||||
@@ -63,12 +69,18 @@
|
||||
<view class="tab" :class="{ active: activeTab==='home' }" @click="activeTab='home'">
|
||||
<text>首页</text>
|
||||
</view>
|
||||
<view class="tab" :class="{ active: activeTab==='product' }" @click="goProduct">
|
||||
<text>货品</text>
|
||||
</view>
|
||||
<view class="tab primary" @click="onCreateOrder">
|
||||
<text>开单</text>
|
||||
</view>
|
||||
<view class="tab" :class="{ active: activeTab==='detail' }" @click="activeTab='detail'">
|
||||
<text>明细</text>
|
||||
</view>
|
||||
<view class="tab" :class="{ active: activeTab==='report' }" @click="activeTab='report'">
|
||||
<text>报表</text>
|
||||
</view>
|
||||
<view class="tab" :class="{ active: activeTab==='me' }" @click="activeTab='me'">
|
||||
<text>我的</text>
|
||||
</view>
|
||||
@@ -77,20 +89,17 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { get } from '../../common/http.js'
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
todayAmount: '0.00',
|
||||
monthProfit: '0.00',
|
||||
stockQty: '0.00',
|
||||
kpi: { todaySales: '0.00', monthSales: '0.00', monthProfit: '0.00', stockCount: '0' },
|
||||
activeTab: 'home',
|
||||
notices: [
|
||||
{ text: '选材精工:不锈钢、合金钢,耐腐蚀更耐用', tag: '品质' },
|
||||
{ text: '表面工艺:电镀锌/镀镍/发黑处理,性能均衡', tag: '工艺' },
|
||||
{ text: '库存齐全:螺丝、螺母、垫圈、膨胀螺栓等现货', tag: '库存' },
|
||||
{ text: '企业采购支持:批量优惠,次日发货', tag: '服务' }
|
||||
],
|
||||
notices: [],
|
||||
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: '💳' },
|
||||
@@ -103,23 +112,63 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
onLoad() {
|
||||
this.fetchMetrics()
|
||||
this.fetchNotices()
|
||||
},
|
||||
methods: {
|
||||
async fetchMetrics() {
|
||||
try {
|
||||
const d = await get('/api/dashboard/overview')
|
||||
const toNum = v => (typeof v === 'number' ? v : Number(v || 0))
|
||||
this.kpi = {
|
||||
...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) {
|
||||
// 忽略错误,保留默认值
|
||||
}
|
||||
},
|
||||
async fetchNotices() {
|
||||
this.loadingNotices = true
|
||||
this.noticeError = ''
|
||||
try {
|
||||
const list = await get('/api/notices')
|
||||
this.notices = Array.isArray(list) ? list.map(n => ({
|
||||
text: n.content || n.title || '',
|
||||
tag: n.tag || ''
|
||||
})) : []
|
||||
} catch (e) {
|
||||
this.noticeError = (e && e.message) || '公告加载失败'
|
||||
} finally {
|
||||
this.loadingNotices = false
|
||||
}
|
||||
},
|
||||
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.showToast({ title: '开单(开发中)', icon: 'none' })
|
||||
uni.navigateTo({ url: '/pages/order/create' })
|
||||
},
|
||||
onNoticeTap(n) {
|
||||
uni.showModal({
|
||||
title: '公告',
|
||||
content: n.text,
|
||||
title: '广告',
|
||||
content: n && (n.text || n.title || n.content) || '',
|
||||
showCancel: false
|
||||
})
|
||||
},
|
||||
onNoticeList() {
|
||||
uni.showToast({ title: '公告列表(开发中)', icon: 'none' })
|
||||
},
|
||||
|
||||
onIconError(item) {
|
||||
item.img = ''
|
||||
}
|
||||
@@ -173,7 +222,7 @@
|
||||
.notice-item { display: flex; align-items: center; gap: 12rpx; min-height: 72rpx; }
|
||||
.notice-text { color: #4b3e19; font-size: 28rpx; line-height: 36rpx; font-weight: 600; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
|
||||
.notice-tag { color: #B4880F; font-size: 22rpx; padding: 4rpx 10rpx; border-radius: 999rpx; background: rgba(215,167,46,0.18); }
|
||||
.notice-right { flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center; min-width: 72rpx; height: 44rpx; color: #B4880F; font-size: 26rpx; padding-left: 8rpx; }
|
||||
|
||||
|
||||
/* 分割标题 */
|
||||
.section-title { display: flex; align-items: center; gap: 16rpx; padding: 10rpx 28rpx 0; }
|
||||
|
||||
224
frontend/pages/order/create.vue
Normal file
224
frontend/pages/order/create.vue
Normal file
@@ -0,0 +1,224 @@
|
||||
<template>
|
||||
<view class="order">
|
||||
<!-- 顶部 Tab -->
|
||||
<view class="tabs">
|
||||
<text :class="{ active: biz==='sale' }" @click="switchBiz('sale')">销售</text>
|
||||
<text :class="{ active: biz==='purchase' }" @click="switchBiz('purchase')">进货</text>
|
||||
<text :class="{ active: biz==='income' }" @click="switchBiz('income')">其他收入</text>
|
||||
<text :class="{ active: biz==='expense' }" @click="switchBiz('expense')">其他支出</text>
|
||||
</view>
|
||||
|
||||
<!-- 子类目按钮 -->
|
||||
<view class="subtabs" v-if="biz==='sale'">
|
||||
<button class="subbtn" :class="{ active: saleType==='out' }" @click="saleType='out'">出货</button>
|
||||
<button class="subbtn" :class="{ active: saleType==='return' }" @click="saleType='return'">退货</button>
|
||||
<button class="subbtn" :class="{ active: saleType==='collect' }" @click="saleType='collect'">收款</button>
|
||||
</view>
|
||||
<view class="subtabs" v-else-if="biz==='purchase'">
|
||||
<button class="subbtn" :class="{ active: purchaseType==='in' }" @click="purchaseType='in'">进货</button>
|
||||
<button class="subbtn" :class="{ active: purchaseType==='return' }" @click="purchaseType='return'">退货</button>
|
||||
<button class="subbtn" :class="{ active: purchaseType==='pay' }" @click="purchaseType='pay'">付款</button>
|
||||
</view>
|
||||
|
||||
<!-- 日期与客户 -->
|
||||
<picker mode="date" :value="order.orderTime" @change="onDateChange">
|
||||
<view class="field">
|
||||
<text class="label">时间</text>
|
||||
<text class="value">{{ order.orderTime }}</text>
|
||||
</view>
|
||||
</picker>
|
||||
<view class="field" v-if="biz==='sale'" @click="chooseCustomer">
|
||||
<text class="label">客户</text>
|
||||
<text class="value">{{ customerLabel }}</text>
|
||||
</view>
|
||||
<view class="field" v-else-if="biz==='purchase'" @click="chooseSupplier">
|
||||
<text class="label">供应商</text>
|
||||
<text class="value">{{ supplierLabel }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 已选商品与合计(销售/进货) -->
|
||||
<view v-if="biz==='sale' || biz==='purchase'">
|
||||
<view class="summary">
|
||||
<text>选中货品({{ totalQuantity }})</text>
|
||||
<text>合计金额:¥ {{ totalAmount.toFixed(2) }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 加号添加商品 -->
|
||||
<view class="add" @click="chooseProduct">+</view>
|
||||
</view>
|
||||
|
||||
<!-- 其它收入/支出 表单 -->
|
||||
<view v-else>
|
||||
<view class="chips">
|
||||
<view v-for="c in (biz==='income' ? incomeCategories : expenseCategories)" :key="c.key" class="chip" :class="{ active: activeCategory===c.key }" @click="activeCategory=c.key">{{ c.label }}</view>
|
||||
</view>
|
||||
<view class="field" @click="chooseCounterparty">
|
||||
<text class="label">往来单位</text>
|
||||
<text class="value">{{ counterpartyLabel }}</text>
|
||||
</view>
|
||||
<view class="field" @click="chooseAccount">
|
||||
<text class="label">结算账户</text>
|
||||
<text class="value">{{ accountLabel }}</text>
|
||||
</view>
|
||||
<view class="field">
|
||||
<text class="label">金额</text>
|
||||
<input class="value" type="digit" v-model.number="trxAmount" placeholder="0.00" />
|
||||
</view>
|
||||
<view class="textarea">
|
||||
<textarea v-model="order.remark" maxlength="200" placeholder="备注(最多输入200个字)"></textarea>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 购物车空态 -->
|
||||
<view class="empty" v-if="!items.length">
|
||||
<image src="/static/logo.png" mode="widthFix" class="empty-img"></image>
|
||||
<text class="empty-text">购物车里空空如也</text>
|
||||
<text class="empty-sub">扫描或点击 “+” 选择商品吧</text>
|
||||
</view>
|
||||
|
||||
<!-- 商品列表 -->
|
||||
<view v-else class="list">
|
||||
<view class="row" v-for="(it, idx) in items" :key="idx">
|
||||
<view class="col name">{{ it.productName }}</view>
|
||||
<view class="col qty">
|
||||
<input type="number" v-model.number="it.quantity" @input="recalc()" />
|
||||
</view>
|
||||
<view class="col price">
|
||||
<input type="number" v-model.number="it.unitPrice" @input="recalc()" />
|
||||
</view>
|
||||
<view class="col amount">¥ {{ (Number(it.quantity)*Number(it.unitPrice)).toFixed(2) }}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部提交栏 -->
|
||||
<view class="bottom">
|
||||
<button class="ghost" @click="saveAndReset">再记一笔</button>
|
||||
<button class="primary" @click="submit">保存</button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { post } from '../../common/http.js'
|
||||
import { INCOME_CATEGORIES, EXPENSE_CATEGORIES } from '../../common/constants.js'
|
||||
|
||||
function todayString() {
|
||||
const d = new Date()
|
||||
const m = (d.getMonth()+1).toString().padStart(2,'0')
|
||||
const day = d.getDate().toString().padStart(2,'0')
|
||||
return `${d.getFullYear()}-${m}-${day}`
|
||||
}
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
biz: 'sale',
|
||||
saleType: 'out',
|
||||
purchaseType: 'in',
|
||||
order: {
|
||||
orderTime: todayString(),
|
||||
customerId: null,
|
||||
supplierId: null,
|
||||
remark: ''
|
||||
},
|
||||
customerName: '',
|
||||
supplierName: '',
|
||||
items: [],
|
||||
activeCategory: 'sale_income',
|
||||
trxAmount: 0,
|
||||
selectedAccountId: null,
|
||||
selectedAccountName: ''
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
totalQuantity() {
|
||||
return this.items.reduce((s, it) => s + Number(it.quantity || 0), 0)
|
||||
},
|
||||
totalAmount() {
|
||||
return this.items.reduce((s, it) => s + Number(it.quantity || 0) * Number(it.unitPrice || 0), 0)
|
||||
},
|
||||
customerLabel() { return this.customerName || '零售客户' },
|
||||
supplierLabel() { return this.supplierName || '零散供应商' },
|
||||
incomeCategories() { return INCOME_CATEGORIES },
|
||||
expenseCategories() { return EXPENSE_CATEGORIES },
|
||||
accountLabel() { return this.selectedAccountName || '现金' },
|
||||
counterpartyLabel() { return this.customerName || this.supplierName || '—' }
|
||||
},
|
||||
methods: {
|
||||
switchBiz(type) { this.biz = type },
|
||||
onDateChange(e) { this.order.orderTime = e.detail.value },
|
||||
chooseCustomer() {
|
||||
uni.navigateTo({ url: '/pages/customer/select' })
|
||||
},
|
||||
chooseSupplier() { uni.navigateTo({ url: '/pages/supplier/select' }) },
|
||||
chooseProduct() {
|
||||
uni.navigateTo({ url: '/pages/product/select' })
|
||||
},
|
||||
chooseAccount() { uni.navigateTo({ url: '/pages/account/select' }) },
|
||||
chooseCounterparty() {
|
||||
if (this.biz==='income' || this.biz==='expense') {
|
||||
uni.navigateTo({ url: '/pages/customer/select' })
|
||||
}
|
||||
},
|
||||
recalc() { this.$forceUpdate() },
|
||||
async submit() {
|
||||
const isSaleOrPurchase = (this.biz==='sale' || this.biz==='purchase')
|
||||
const payload = isSaleOrPurchase ? {
|
||||
type: this.biz === 'sale' ? (this.saleType) : ('purchase.' + this.purchaseType),
|
||||
orderTime: this.order.orderTime,
|
||||
customerId: this.order.customerId,
|
||||
supplierId: this.order.supplierId,
|
||||
items: this.items.map(it => ({ productId: it.productId, quantity: Number(it.quantity||0), unitPrice: Number(it.unitPrice||0) })),
|
||||
amount: this.totalAmount
|
||||
} : {
|
||||
type: this.biz,
|
||||
category: this.activeCategory,
|
||||
counterpartyId: this.order.customerId || null,
|
||||
accountId: this.selectedAccountId || null,
|
||||
amount: Number(this.trxAmount||0),
|
||||
txTime: this.order.orderTime,
|
||||
remark: this.order.remark
|
||||
}
|
||||
try {
|
||||
const url = isSaleOrPurchase ? '/api/orders' : '/api/other-transactions'
|
||||
await post(url, payload)
|
||||
uni.showToast({ title: '已保存', icon: 'success' })
|
||||
setTimeout(() => { uni.navigateBack() }, 600)
|
||||
} catch (e) {
|
||||
uni.showToast({ title: e && e.message || '保存失败', icon: 'none' })
|
||||
}
|
||||
},
|
||||
saveAndReset() {
|
||||
this.items = []
|
||||
this.trxAmount = 0
|
||||
this.order.remark = ''
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.order { padding-bottom: 140rpx; }
|
||||
.tabs { display: flex; justify-content: space-around; padding: 16rpx 24rpx; }
|
||||
.tabs text { color: #666; }
|
||||
.tabs text.active { color: #333; font-weight: 700; }
|
||||
.subtabs { display: flex; gap: 16rpx; padding: 0 24rpx 16rpx; }
|
||||
.subbtn { padding: 10rpx 20rpx; border-radius: 999rpx; background: #f4f4f4; color: #666; }
|
||||
.subbtn.active { background: #ffe69a; color: #3f320f; }
|
||||
.field { display:flex; justify-content: space-between; padding: 22rpx 24rpx; background: #fff; border-bottom: 1rpx solid #eee; }
|
||||
.label { color:#666; }
|
||||
.value { color:#333; }
|
||||
.summary { display:flex; justify-content: space-between; padding: 22rpx 24rpx; color:#333; }
|
||||
.add { margin: 24rpx auto; width: 120rpx; height: 120rpx; border-radius: 20rpx; background: #c7eef7; color:#16a1c4; font-size: 72rpx; display:flex; align-items:center; justify-content:center; }
|
||||
.empty { display:flex; flex-direction: column; align-items:center; padding: 60rpx 0; color:#888; }
|
||||
.empty-img { width: 220rpx; margin-bottom: 20rpx; }
|
||||
.empty-text { margin-bottom: 8rpx; }
|
||||
.list { background:#fff; }
|
||||
.row { display:grid; grid-template-columns: 1.5fr 1fr 1fr 1fr; gap: 12rpx; padding: 16rpx 12rpx; align-items:center; border-bottom: 1rpx solid #f3f3f3; }
|
||||
.col.name { padding-left: 12rpx; }
|
||||
.col.amount { text-align:right; padding-right: 12rpx; color:#333; }
|
||||
.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; }
|
||||
</style>
|
||||
|
||||
|
||||
67
frontend/pages/product/categories.vue
Normal file
67
frontend/pages/product/categories.vue
Normal 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>
|
||||
|
||||
|
||||
|
||||
|
||||
213
frontend/pages/product/form.vue
Normal file
213
frontend/pages/product/form.vue
Normal 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>
|
||||
|
||||
|
||||
133
frontend/pages/product/list.vue
Normal file
133
frontend/pages/product/list.vue
Normal 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>
|
||||
|
||||
|
||||
|
||||
|
||||
50
frontend/pages/product/select.vue
Normal file
50
frontend/pages/product/select.vue
Normal file
@@ -0,0 +1,50 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<view class="search">
|
||||
<input v-model="kw" placeholder="搜索商品名称/编码" @confirm="search" />
|
||||
<button size="mini" @click="search">搜索</button>
|
||||
</view>
|
||||
<scroll-view scroll-y class="list">
|
||||
<view class="item" v-for="p in products" :key="p.id" @click="select(p)">
|
||||
<view class="name">{{ p.name }}</view>
|
||||
<view class="meta">{{ p.code }} · 库存:{{ p.stock || 0 }}</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { get } from '../../common/http.js'
|
||||
export default {
|
||||
data() { return { kw: '', products: [] } },
|
||||
onLoad() { this.search() },
|
||||
methods: {
|
||||
async search() {
|
||||
try {
|
||||
const res = await get('/api/products', { kw: this.kw, page: 1, size: 50 })
|
||||
this.products = Array.isArray(res?.list) ? res.list : (Array.isArray(res) ? res : [])
|
||||
} catch(e) { uni.showToast({ title: '加载失败', icon: 'none' }) }
|
||||
},
|
||||
select(p) {
|
||||
const opener = getCurrentPages()[getCurrentPages().length-2]
|
||||
if (opener && opener.$vm && opener.$vm.items) {
|
||||
opener.$vm.items.push({ productId: p.id, productName: p.name, quantity: 1, unitPrice: Number(p.price || 0) })
|
||||
}
|
||||
uni.navigateBack()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.page { display:flex; flex-direction: column; height: 100vh; }
|
||||
.search { display:flex; gap: 12rpx; padding: 16rpx; background:#fff; }
|
||||
.search input { flex:1; background:#f6f6f6; border-radius: 12rpx; padding: 12rpx; }
|
||||
.list { flex:1; }
|
||||
.item { padding: 20rpx 24rpx; background:#fff; border-bottom: 1rpx solid #f1f1f1; }
|
||||
.name { color:#333; margin-bottom: 6rpx; }
|
||||
.meta { color:#888; font-size: 24rpx; }
|
||||
</style>
|
||||
|
||||
|
||||
|
||||
45
frontend/pages/product/settings.vue
Normal file
45
frontend/pages/product/settings.vue
Normal 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>
|
||||
|
||||
|
||||
|
||||
|
||||
67
frontend/pages/product/units.vue
Normal file
67
frontend/pages/product/units.vue
Normal 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>
|
||||
|
||||
|
||||
|
||||
|
||||
51
frontend/pages/supplier/select.vue
Normal file
51
frontend/pages/supplier/select.vue
Normal file
@@ -0,0 +1,51 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<view class="search">
|
||||
<input v-model="kw" placeholder="搜索供应商名称/电话" @confirm="search" />
|
||||
<button size="mini" @click="search">搜索</button>
|
||||
</view>
|
||||
<scroll-view scroll-y class="list">
|
||||
<view class="item" v-for="s in suppliers" :key="s.id" @click="select(s)">
|
||||
<view class="name">{{ s.name }}</view>
|
||||
<view class="meta">{{ s.mobile || '—' }}</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { get } from '../../common/http.js'
|
||||
export default {
|
||||
data() { return { kw: '', suppliers: [] } },
|
||||
onLoad() { this.search() },
|
||||
methods: {
|
||||
async search() {
|
||||
try {
|
||||
const res = await get('/api/suppliers', { kw: this.kw, page: 1, size: 50 })
|
||||
this.suppliers = Array.isArray(res?.list) ? res.list : (Array.isArray(res) ? res : [])
|
||||
} catch(e) { uni.showToast({ title: '加载失败', icon: 'none' }) }
|
||||
},
|
||||
select(s) {
|
||||
const opener = getCurrentPages()[getCurrentPages().length-2]
|
||||
if (opener && opener.$vm) {
|
||||
opener.$vm.order.supplierId = s.id
|
||||
opener.$vm.supplierName = s.name
|
||||
}
|
||||
uni.navigateBack()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.page { display:flex; flex-direction: column; height: 100vh; }
|
||||
.search { display:flex; gap: 12rpx; padding: 16rpx; background:#fff; }
|
||||
.search input { flex:1; background:#f6f6f6; border-radius: 12rpx; padding: 12rpx; }
|
||||
.list { flex:1; }
|
||||
.item { padding: 20rpx 24rpx; background:#fff; border-bottom: 1rpx solid #f1f1f1; }
|
||||
.name { color:#333; margin-bottom: 6rpx; }
|
||||
.meta { color:#888; font-size: 24rpx; }
|
||||
</style>
|
||||
|
||||
|
||||
|
||||
@@ -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>\n","import App from './App'\n\n// #ifndef VUE3\nimport Vue from 'vue'\nimport './uni.promisify.adaptor'\nVue.config.productionTip = false\nApp.mpType = 'app'\nconst app = new Vue({\n ...App\n})\napp.$mount()\n// #endif\n\n// #ifdef VUE3\nimport { createSSRApp } from 'vue'\nexport function createApp() {\n const app = createSSRApp(App)\n return {\n app\n }\n}\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;;;"}
|
||||
@@ -1 +1 @@
|
||||
{"version":3,"file":"assets.js","sources":["../../../static/metal-bg.jpg"],"sourcesContent":["export default \"/static/metal-bg.jpg\""],"names":[],"mappings":";AAAA,MAAe,aAAA;;"}
|
||||
{"version":3,"file":"assets.js","sources":["../../../../../../static/metal-bg.jpg","static/logo.png"],"sourcesContent":["export default \"/static/metal-bg.jpg\"","export default \"__VITE_ASSET__46719607__\""],"names":[],"mappings":";AAAA,MAAe,eAAA;ACAf,MAAe,aAAA;;;"}
|
||||
1
frontend/unpackage/dist/dev/.sourcemap/mp-weixin/common/config.js.map
vendored
Normal file
1
frontend/unpackage/dist/dev/.sourcemap/mp-weixin/common/config.js.map
vendored
Normal file
@@ -0,0 +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://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;;;;;;"}
|
||||
1
frontend/unpackage/dist/dev/.sourcemap/mp-weixin/common/constants.js.map
vendored
Normal file
1
frontend/unpackage/dist/dev/.sourcemap/mp-weixin/common/constants.js.map
vendored
Normal file
@@ -0,0 +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\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;;;"}
|
||||
1
frontend/unpackage/dist/dev/.sourcemap/mp-weixin/common/http.js.map
vendored
Normal file
1
frontend/unpackage/dist/dev/.sourcemap/mp-weixin/common/http.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
frontend/unpackage/dist/dev/.sourcemap/mp-weixin/components/ImageUploader.js.map
vendored
Normal file
1
frontend/unpackage/dist/dev/.sourcemap/mp-weixin/components/ImageUploader.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
1
frontend/unpackage/dist/dev/.sourcemap/mp-weixin/pages/account/select.js.map
vendored
Normal file
1
frontend/unpackage/dist/dev/.sourcemap/mp-weixin/pages/account/select.js.map
vendored
Normal file
@@ -0,0 +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\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;"}
|
||||
1
frontend/unpackage/dist/dev/.sourcemap/mp-weixin/pages/customer/select.js.map
vendored
Normal file
1
frontend/unpackage/dist/dev/.sourcemap/mp-weixin/pages/customer/select.js.map
vendored
Normal file
@@ -0,0 +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\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;"}
|
||||
1
frontend/unpackage/dist/dev/.sourcemap/mp-weixin/pages/detail/index.js.map
vendored
Normal file
1
frontend/unpackage/dist/dev/.sourcemap/mp-weixin/pages/detail/index.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
frontend/unpackage/dist/dev/.sourcemap/mp-weixin/pages/order/create.js.map
vendored
Normal file
1
frontend/unpackage/dist/dev/.sourcemap/mp-weixin/pages/order/create.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
1
frontend/unpackage/dist/dev/.sourcemap/mp-weixin/pages/product/categories.js.map
vendored
Normal file
1
frontend/unpackage/dist/dev/.sourcemap/mp-weixin/pages/product/categories.js.map
vendored
Normal 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;"}
|
||||
1
frontend/unpackage/dist/dev/.sourcemap/mp-weixin/pages/product/edit.js.map
vendored
Normal file
1
frontend/unpackage/dist/dev/.sourcemap/mp-weixin/pages/product/edit.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
1
frontend/unpackage/dist/dev/.sourcemap/mp-weixin/pages/product/form.js.map
vendored
Normal file
1
frontend/unpackage/dist/dev/.sourcemap/mp-weixin/pages/product/form.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
1
frontend/unpackage/dist/dev/.sourcemap/mp-weixin/pages/product/list.js.map
vendored
Normal file
1
frontend/unpackage/dist/dev/.sourcemap/mp-weixin/pages/product/list.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
1
frontend/unpackage/dist/dev/.sourcemap/mp-weixin/pages/product/select.js.map
vendored
Normal file
1
frontend/unpackage/dist/dev/.sourcemap/mp-weixin/pages/product/select.js.map
vendored
Normal file
@@ -0,0 +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\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;"}
|
||||
1
frontend/unpackage/dist/dev/.sourcemap/mp-weixin/pages/product/settings.js.map
vendored
Normal file
1
frontend/unpackage/dist/dev/.sourcemap/mp-weixin/pages/product/settings.js.map
vendored
Normal 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;"}
|
||||
1
frontend/unpackage/dist/dev/.sourcemap/mp-weixin/pages/product/units.js.map
vendored
Normal file
1
frontend/unpackage/dist/dev/.sourcemap/mp-weixin/pages/product/units.js.map
vendored
Normal 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;"}
|
||||
1
frontend/unpackage/dist/dev/.sourcemap/mp-weixin/pages/supplier/select.js.map
vendored
Normal file
1
frontend/unpackage/dist/dev/.sourcemap/mp-weixin/pages/supplier/select.js.map
vendored
Normal file
@@ -0,0 +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\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;"}
|
||||
10
frontend/unpackage/dist/dev/mp-weixin/app.js
vendored
10
frontend/unpackage/dist/dev/mp-weixin/app.js
vendored
@@ -3,6 +3,16 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
||||
const common_vendor = require("./common/vendor.js");
|
||||
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";
|
||||
}
|
||||
const _sfc_main = {
|
||||
onLaunch: function() {
|
||||
|
||||
12
frontend/unpackage/dist/dev/mp-weixin/app.json
vendored
12
frontend/unpackage/dist/dev/mp-weixin/app.json
vendored
@@ -1,6 +1,16 @@
|
||||
{
|
||||
"pages": [
|
||||
"pages/index/index"
|
||||
"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"
|
||||
],
|
||||
"window": {
|
||||
"navigationBarTextStyle": "black",
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
"use strict";
|
||||
const _imports_0 = "/static/metal-bg.jpg";
|
||||
exports._imports_0 = _imports_0;
|
||||
const _imports_0$1 = "/static/metal-bg.jpg";
|
||||
const _imports_0 = "/static/logo.png";
|
||||
exports._imports_0 = _imports_0$1;
|
||||
exports._imports_0$1 = _imports_0;
|
||||
//# sourceMappingURL=../../.sourcemap/mp-weixin/common/assets.js.map
|
||||
|
||||
23
frontend/unpackage/dist/dev/mp-weixin/common/config.js
vendored
Normal file
23
frontend/unpackage/dist/dev/mp-weixin/common/config.js
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
"use strict";
|
||||
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://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 || "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;
|
||||
//# sourceMappingURL=../../.sourcemap/mp-weixin/common/config.js.map
|
||||
18
frontend/unpackage/dist/dev/mp-weixin/common/constants.js
vendored
Normal file
18
frontend/unpackage/dist/dev/mp-weixin/common/constants.js
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
"use strict";
|
||||
const INCOME_CATEGORIES = [
|
||||
{ key: "sale_income", label: "销售收入" },
|
||||
{ key: "operation_income", label: "经营所得" },
|
||||
{ key: "interest_income", label: "利息收入" },
|
||||
{ key: "investment_income", label: "投资收入" },
|
||||
{ key: "other_income", label: "其它收入" }
|
||||
];
|
||||
const EXPENSE_CATEGORIES = [
|
||||
{ key: "operation_expense", label: "经营支出" },
|
||||
{ key: "office_supplies", label: "办公用品" },
|
||||
{ key: "rent", label: "房租" },
|
||||
{ key: "interest_expense", label: "利息支出" },
|
||||
{ key: "other_expense", label: "其它支出" }
|
||||
];
|
||||
exports.EXPENSE_CATEGORIES = EXPENSE_CATEGORIES;
|
||||
exports.INCOME_CATEGORIES = INCOME_CATEGORIES;
|
||||
//# sourceMappingURL=../../.sourcemap/mp-weixin/common/constants.js.map
|
||||
104
frontend/unpackage/dist/dev/mp-weixin/common/http.js
vendored
Normal file
104
frontend/unpackage/dist/dev/mp-weixin/common/http.js
vendored
Normal file
@@ -0,0 +1,104 @@
|
||||
"use strict";
|
||||
const common_vendor = require("./vendor.js");
|
||||
const common_config = require("./config.js");
|
||||
function buildUrl(path) {
|
||||
if (!path)
|
||||
return common_config.API_BASE_URL;
|
||||
if (path.startsWith("http"))
|
||||
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;
|
||||
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 = {}) {
|
||||
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: "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
|
||||
@@ -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) => {
|
||||
@@ -88,6 +88,40 @@ const looseToNumber = (val) => {
|
||||
const n = parseFloat(val);
|
||||
return isNaN(n) ? val : n;
|
||||
};
|
||||
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);
|
||||
};
|
||||
@@ -1283,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;
|
||||
@@ -2074,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)) {
|
||||
@@ -3688,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) {
|
||||
@@ -4928,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];
|
||||
}
|
||||
@@ -4968,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;
|
||||
@@ -5094,10 +5199,34 @@ function vFor(source, renderItem) {
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
function withModelModifiers(fn, { number, trim }, isComponent = false) {
|
||||
if (isComponent) {
|
||||
return (...args) => {
|
||||
if (trim) {
|
||||
args = args.map((a) => a.trim());
|
||||
} else if (number) {
|
||||
args = args.map(toNumber);
|
||||
}
|
||||
return fn(...args);
|
||||
};
|
||||
}
|
||||
return (event) => {
|
||||
const value = event.detail.value;
|
||||
if (trim) {
|
||||
event.detail.value = value.trim();
|
||||
} else if (number) {
|
||||
event.detail.value = toNumber(value);
|
||||
}
|
||||
return fn(event);
|
||||
};
|
||||
}
|
||||
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");
|
||||
return createVueApp(rootComponent, rootProps).use(plugin);
|
||||
@@ -5419,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);
|
||||
})));
|
||||
};
|
||||
}
|
||||
@@ -5741,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;
|
||||
@@ -5750,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);
|
||||
}
|
||||
@@ -5819,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);
|
||||
})));
|
||||
@@ -6428,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(() => {
|
||||
@@ -6442,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);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -6917,9 +7046,9 @@ function isConsoleWritable() {
|
||||
return isWritable;
|
||||
}
|
||||
function initRuntimeSocketService() {
|
||||
const hosts = "198.18.0.1,192.168.31.107,127.0.0.1";
|
||||
const hosts = "198.18.0.1,192.168.31.193,127.0.0.1";
|
||||
const port = "8090";
|
||||
const id = "mp-weixin_BJ7qAd";
|
||||
const id = "mp-weixin_UjOtWQ";
|
||||
const lazy = typeof swan !== "undefined";
|
||||
let restoreError = lazy ? () => {
|
||||
} : initOnError();
|
||||
@@ -7870,6 +7999,10 @@ exports.createSSRApp = createSSRApp;
|
||||
exports.e = e;
|
||||
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
|
||||
|
||||
144
frontend/unpackage/dist/dev/mp-weixin/components/ImageUploader.js
vendored
Normal file
144
frontend/unpackage/dist/dev/mp-weixin/components/ImageUploader.js
vendored
Normal 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
|
||||
4
frontend/unpackage/dist/dev/mp-weixin/components/ImageUploader.json
vendored
Normal file
4
frontend/unpackage/dist/dev/mp-weixin/components/ImageUploader.json
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {}
|
||||
}
|
||||
1
frontend/unpackage/dist/dev/mp-weixin/components/ImageUploader.wxml
vendored
Normal file
1
frontend/unpackage/dist/dev/mp-weixin/components/ImageUploader.wxml
vendored
Normal 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>
|
||||
15
frontend/unpackage/dist/dev/mp-weixin/components/ImageUploader.wxss
vendored
Normal file
15
frontend/unpackage/dist/dev/mp-weixin/components/ImageUploader.wxss
vendored
Normal 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;
|
||||
}
|
||||
47
frontend/unpackage/dist/dev/mp-weixin/pages/account/select.js
vendored
Normal file
47
frontend/unpackage/dist/dev/mp-weixin/pages/account/select.js
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
"use strict";
|
||||
const common_vendor = require("../../common/vendor.js");
|
||||
const common_http = require("../../common/http.js");
|
||||
const TYPE_MAP = { cash: "现金", bank: "银行", alipay: "支付宝", wechat: "微信", other: "其他" };
|
||||
const _sfc_main = {
|
||||
data() {
|
||||
return { accounts: [] };
|
||||
},
|
||||
async onLoad() {
|
||||
try {
|
||||
const res = await common_http.get("/api/accounts");
|
||||
this.accounts = Array.isArray(res) ? res : (res == null ? void 0 : res.list) || [];
|
||||
} catch (e) {
|
||||
common_vendor.index.showToast({ title: "加载失败", icon: "none" });
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
select(a) {
|
||||
const opener = getCurrentPages()[getCurrentPages().length - 2];
|
||||
if (opener && opener.$vm) {
|
||||
opener.$vm.selectedAccountId = a.id;
|
||||
opener.$vm.selectedAccountName = a.name;
|
||||
}
|
||||
common_vendor.index.navigateBack();
|
||||
},
|
||||
typeLabel(t) {
|
||||
return TYPE_MAP[t] || t;
|
||||
}
|
||||
}
|
||||
};
|
||||
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
|
||||
return {
|
||||
a: common_vendor.f($data.accounts, (a, k0, i0) => {
|
||||
var _a;
|
||||
return {
|
||||
a: common_vendor.t(a.name),
|
||||
b: common_vendor.t($options.typeLabel(a.type)),
|
||||
c: common_vendor.t(((_a = a.balance) == null ? void 0 : _a.toFixed) ? a.balance.toFixed(2) : a.balance),
|
||||
d: a.id,
|
||||
e: common_vendor.o(($event) => $options.select(a), a.id)
|
||||
};
|
||||
})
|
||||
};
|
||||
}
|
||||
const MiniProgramPage = /* @__PURE__ */ common_vendor._export_sfc(_sfc_main, [["render", _sfc_render]]);
|
||||
wx.createPage(MiniProgramPage);
|
||||
//# sourceMappingURL=../../../.sourcemap/mp-weixin/pages/account/select.js.map
|
||||
4
frontend/unpackage/dist/dev/mp-weixin/pages/account/select.json
vendored
Normal file
4
frontend/unpackage/dist/dev/mp-weixin/pages/account/select.json
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"navigationBarTitleText": "选择账户",
|
||||
"usingComponents": {}
|
||||
}
|
||||
1
frontend/unpackage/dist/dev/mp-weixin/pages/account/select.wxml
vendored
Normal file
1
frontend/unpackage/dist/dev/mp-weixin/pages/account/select.wxml
vendored
Normal file
@@ -0,0 +1 @@
|
||||
<view class="page"><scroll-view scroll-y class="list"><view wx:for="{{a}}" wx:for-item="a" wx:key="d" class="item" bindtap="{{a.e}}"><view class="name">{{a.a}}</view><view class="meta">{{a.b}} · 余额:{{a.c}}</view></view></scroll-view></view>
|
||||
11
frontend/unpackage/dist/dev/mp-weixin/pages/account/select.wxss
vendored
Normal file
11
frontend/unpackage/dist/dev/mp-weixin/pages/account/select.wxss
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
|
||||
.page { display:flex; flex-direction: column; height: 100vh;
|
||||
}
|
||||
.list { flex:1;
|
||||
}
|
||||
.item { padding: 20rpx 24rpx; background:#fff; border-bottom: 1rpx solid #f1f1f1;
|
||||
}
|
||||
.name { color:#333; margin-bottom: 6rpx;
|
||||
}
|
||||
.meta { color:#888; font-size: 24rpx;
|
||||
}
|
||||
48
frontend/unpackage/dist/dev/mp-weixin/pages/customer/select.js
vendored
Normal file
48
frontend/unpackage/dist/dev/mp-weixin/pages/customer/select.js
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
"use strict";
|
||||
const common_vendor = require("../../common/vendor.js");
|
||||
const common_http = require("../../common/http.js");
|
||||
const _sfc_main = {
|
||||
data() {
|
||||
return { kw: "", customers: [] };
|
||||
},
|
||||
onLoad() {
|
||||
this.search();
|
||||
},
|
||||
methods: {
|
||||
async search() {
|
||||
try {
|
||||
const res = await common_http.get("/api/customers", { kw: this.kw, page: 1, size: 50 });
|
||||
this.customers = Array.isArray(res == null ? void 0 : res.list) ? res.list : Array.isArray(res) ? res : [];
|
||||
} catch (e) {
|
||||
common_vendor.index.showToast({ title: "加载失败", icon: "none" });
|
||||
}
|
||||
},
|
||||
select(c) {
|
||||
const opener = getCurrentPages()[getCurrentPages().length - 2];
|
||||
if (opener && opener.$vm) {
|
||||
opener.$vm.order.customerId = c.id;
|
||||
opener.$vm.customerName = c.name;
|
||||
}
|
||||
common_vendor.index.navigateBack();
|
||||
}
|
||||
}
|
||||
};
|
||||
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
|
||||
return {
|
||||
a: common_vendor.o((...args) => $options.search && $options.search(...args)),
|
||||
b: $data.kw,
|
||||
c: common_vendor.o(($event) => $data.kw = $event.detail.value),
|
||||
d: common_vendor.o((...args) => $options.search && $options.search(...args)),
|
||||
e: common_vendor.f($data.customers, (c, k0, i0) => {
|
||||
return {
|
||||
a: common_vendor.t(c.name),
|
||||
b: common_vendor.t(c.mobile || "—"),
|
||||
c: c.id,
|
||||
d: common_vendor.o(($event) => $options.select(c), c.id)
|
||||
};
|
||||
})
|
||||
};
|
||||
}
|
||||
const MiniProgramPage = /* @__PURE__ */ common_vendor._export_sfc(_sfc_main, [["render", _sfc_render]]);
|
||||
wx.createPage(MiniProgramPage);
|
||||
//# sourceMappingURL=../../../.sourcemap/mp-weixin/pages/customer/select.js.map
|
||||
4
frontend/unpackage/dist/dev/mp-weixin/pages/customer/select.json
vendored
Normal file
4
frontend/unpackage/dist/dev/mp-weixin/pages/customer/select.json
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"navigationBarTitleText": "选择客户",
|
||||
"usingComponents": {}
|
||||
}
|
||||
1
frontend/unpackage/dist/dev/mp-weixin/pages/customer/select.wxml
vendored
Normal file
1
frontend/unpackage/dist/dev/mp-weixin/pages/customer/select.wxml
vendored
Normal file
@@ -0,0 +1 @@
|
||||
<view class="page"><view class="search"><input placeholder="搜索客户名称/电话" bindconfirm="{{a}}" value="{{b}}" bindinput="{{c}}"/><button size="mini" bindtap="{{d}}">搜索</button></view><scroll-view scroll-y class="list"><view wx:for="{{e}}" wx:for-item="c" wx:key="c" class="item" bindtap="{{c.d}}"><view class="name">{{c.a}}</view><view class="meta">{{c.b}}</view></view></scroll-view></view>
|
||||
15
frontend/unpackage/dist/dev/mp-weixin/pages/customer/select.wxss
vendored
Normal file
15
frontend/unpackage/dist/dev/mp-weixin/pages/customer/select.wxss
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
|
||||
.page { display:flex; flex-direction: column; height: 100vh;
|
||||
}
|
||||
.search { display:flex; gap: 12rpx; padding: 16rpx; background:#fff;
|
||||
}
|
||||
.search input { flex:1; background:#f6f6f6; border-radius: 12rpx; padding: 12rpx;
|
||||
}
|
||||
.list { flex:1;
|
||||
}
|
||||
.item { padding: 20rpx 24rpx; background:#fff; border-bottom: 1rpx solid #f1f1f1;
|
||||
}
|
||||
.name { color:#333; margin-bottom: 6rpx;
|
||||
}
|
||||
.meta { color:#888; font-size: 24rpx;
|
||||
}
|
||||
135
frontend/unpackage/dist/dev/mp-weixin/pages/detail/index.js
vendored
Normal file
135
frontend/unpackage/dist/dev/mp-weixin/pages/detail/index.js
vendored
Normal file
@@ -0,0 +1,135 @@
|
||||
"use strict";
|
||||
const common_vendor = require("../../common/vendor.js");
|
||||
const common_http = require("../../common/http.js");
|
||||
function toDateString(d) {
|
||||
const m = (d.getMonth() + 1).toString().padStart(2, "0");
|
||||
const day = d.getDate().toString().padStart(2, "0");
|
||||
return `${d.getFullYear()}-${m}-${day}`;
|
||||
}
|
||||
const _sfc_main = {
|
||||
data() {
|
||||
const today = /* @__PURE__ */ new Date();
|
||||
const first = new Date(today.getFullYear(), today.getMonth(), 1);
|
||||
return {
|
||||
range: "month",
|
||||
biz: "sale",
|
||||
sub: "out",
|
||||
kw: "",
|
||||
begin: toDateString(first),
|
||||
end: toDateString(today),
|
||||
list: [],
|
||||
loading: false
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
totalAmount() {
|
||||
return this.list.reduce((s, o) => s + Number(o.amount || 0), 0);
|
||||
}
|
||||
},
|
||||
onLoad() {
|
||||
this.reload();
|
||||
},
|
||||
methods: {
|
||||
setRange(r) {
|
||||
this.range = r;
|
||||
const now = /* @__PURE__ */ new Date();
|
||||
if (r === "today") {
|
||||
this.begin = this.end = toDateString(now);
|
||||
} else if (r === "week") {
|
||||
const day = now.getDay() || 7;
|
||||
const start = new Date(now);
|
||||
start.setDate(now.getDate() - day + 1);
|
||||
this.begin = toDateString(start);
|
||||
this.end = toDateString(now);
|
||||
} else if (r === "month") {
|
||||
const first = new Date(now.getFullYear(), now.getMonth(), 1);
|
||||
this.begin = toDateString(first);
|
||||
this.end = toDateString(now);
|
||||
} else if (r === "year") {
|
||||
const first = new Date(now.getFullYear(), 0, 1);
|
||||
this.begin = toDateString(first);
|
||||
this.end = toDateString(now);
|
||||
}
|
||||
this.reload();
|
||||
},
|
||||
async reload() {
|
||||
this.loading = true;
|
||||
try {
|
||||
const res = await common_http.get("/api/sales/orders", { begin: this.begin, end: this.end, kw: this.kw, sub: this.sub });
|
||||
this.list = Array.isArray(res == null ? void 0 : res.list) ? res.list : Array.isArray(res) ? res : [];
|
||||
} catch (e) {
|
||||
this.list = [];
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
goCreate() {
|
||||
common_vendor.index.navigateTo({ url: "/pages/order/create" });
|
||||
},
|
||||
open(o) {
|
||||
}
|
||||
}
|
||||
};
|
||||
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
|
||||
return common_vendor.e({
|
||||
a: $data.range === "custom" ? 1 : "",
|
||||
b: common_vendor.o(($event) => $options.setRange("custom")),
|
||||
c: $data.range === "week" ? 1 : "",
|
||||
d: common_vendor.o(($event) => $options.setRange("week")),
|
||||
e: $data.range === "today" ? 1 : "",
|
||||
f: common_vendor.o(($event) => $options.setRange("today")),
|
||||
g: $data.range === "month" ? 1 : "",
|
||||
h: common_vendor.o(($event) => $options.setRange("month")),
|
||||
i: $data.range === "year" ? 1 : "",
|
||||
j: common_vendor.o(($event) => $options.setRange("year")),
|
||||
k: $data.biz === "sale" ? 1 : "",
|
||||
l: common_vendor.o(($event) => $data.biz = "sale"),
|
||||
m: $data.biz === "purchase" ? 1 : "",
|
||||
n: common_vendor.o(($event) => $data.biz = "purchase"),
|
||||
o: $data.biz === "collection" ? 1 : "",
|
||||
p: common_vendor.o(($event) => $data.biz = "collection"),
|
||||
q: $data.biz === "capital" ? 1 : "",
|
||||
r: common_vendor.o(($event) => $data.biz = "capital"),
|
||||
s: $data.biz === "inventory" ? 1 : "",
|
||||
t: common_vendor.o(($event) => $data.biz = "inventory"),
|
||||
v: $data.sub === "out" ? 1 : "",
|
||||
w: common_vendor.o(($event) => $data.sub = "out"),
|
||||
x: $data.sub === "return" ? 1 : "",
|
||||
y: common_vendor.o(($event) => $data.sub = "return"),
|
||||
z: $data.sub === "receive" ? 1 : "",
|
||||
A: common_vendor.o(($event) => $data.sub = "receive"),
|
||||
B: common_vendor.o((...args) => $options.goCreate && $options.goCreate(...args)),
|
||||
C: common_vendor.o((...args) => $options.reload && $options.reload(...args)),
|
||||
D: $data.kw,
|
||||
E: common_vendor.o(($event) => $data.kw = $event.detail.value),
|
||||
F: $data.range === "custom"
|
||||
}, $data.range === "custom" ? {
|
||||
G: common_vendor.t($data.begin),
|
||||
H: $data.begin,
|
||||
I: common_vendor.o((e) => {
|
||||
$data.begin = e.detail.value;
|
||||
$options.reload();
|
||||
}),
|
||||
J: common_vendor.t($data.end),
|
||||
K: $data.end,
|
||||
L: common_vendor.o((e) => {
|
||||
$data.end = e.detail.value;
|
||||
$options.reload();
|
||||
})
|
||||
} : {}, {
|
||||
M: common_vendor.t($options.totalAmount.toFixed(2)),
|
||||
N: common_vendor.f($data.list, (o, k0, i0) => {
|
||||
return {
|
||||
a: common_vendor.t(o.orderDate),
|
||||
b: common_vendor.t(o.customerName),
|
||||
c: common_vendor.t(o.orderNo),
|
||||
d: common_vendor.t(Number(o.amount).toFixed(2)),
|
||||
e: o.id,
|
||||
f: common_vendor.o(($event) => $options.open(o), o.id)
|
||||
};
|
||||
})
|
||||
});
|
||||
}
|
||||
const MiniProgramPage = /* @__PURE__ */ common_vendor._export_sfc(_sfc_main, [["render", _sfc_render]]);
|
||||
wx.createPage(MiniProgramPage);
|
||||
//# sourceMappingURL=../../../.sourcemap/mp-weixin/pages/detail/index.js.map
|
||||
4
frontend/unpackage/dist/dev/mp-weixin/pages/detail/index.json
vendored
Normal file
4
frontend/unpackage/dist/dev/mp-weixin/pages/detail/index.json
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"navigationBarTitleText": "明细",
|
||||
"usingComponents": {}
|
||||
}
|
||||
1
frontend/unpackage/dist/dev/mp-weixin/pages/detail/index.wxml
vendored
Normal file
1
frontend/unpackage/dist/dev/mp-weixin/pages/detail/index.wxml
vendored
Normal file
@@ -0,0 +1 @@
|
||||
<view class="detail"><view class="filter-tabs"><view class="{{['tab', a && 'active']}}" bindtap="{{b}}">自定义</view><view class="{{['tab', c && 'active']}}" bindtap="{{d}}">本周</view><view class="{{['tab', e && 'active']}}" bindtap="{{f}}">今日</view><view class="{{['tab', g && 'active']}}" bindtap="{{h}}">本月</view><view class="{{['tab', i && 'active']}}" bindtap="{{j}}">本年</view></view><view class="biz-tabs"><view class="{{['biz', k && 'active']}}" bindtap="{{l}}">销售</view><view class="{{['biz', m && 'active']}}" bindtap="{{n}}">进货</view><view class="{{['biz', o && 'active']}}" bindtap="{{p}}">收款</view><view class="{{['biz', q && 'active']}}" bindtap="{{r}}">资金</view><view class="{{['biz', s && 'active']}}" bindtap="{{t}}">盘点</view></view><view class="card"><view class="subtabs"><view class="{{['sub', v && 'active']}}" bindtap="{{w}}">出货</view><view class="{{['sub', x && 'active']}}" bindtap="{{y}}">退货</view><view class="{{['sub', z && 'active']}}" bindtap="{{A}}">收款</view><view class="plus" bindtap="{{B}}">+</view></view><view class="search"><input placeholder="单据号/客户/名称/规格/备注" bindconfirm="{{C}}" value="{{D}}" bindinput="{{E}}"/></view><view wx:if="{{F}}" class="daterange"><picker mode="date" value="{{H}}" bindchange="{{I}}"><text>{{G}}</text></picker><text class="sep">—</text><picker mode="date" value="{{K}}" bindchange="{{L}}"><text>{{J}}</text></picker></view><view class="total">合计:¥ {{M}}</view><scroll-view scroll-y class="list"><view wx:for="{{N}}" wx:for-item="o" wx:key="e" class="row" bindtap="{{o.f}}"><view class="left"><view class="date">{{o.a}}</view><view class="name">{{o.b}}</view><view class="no">{{o.c}}</view></view><view class="right">¥ {{o.d}}</view></view></scroll-view></view></view>
|
||||
43
frontend/unpackage/dist/dev/mp-weixin/pages/detail/index.wxss
vendored
Normal file
43
frontend/unpackage/dist/dev/mp-weixin/pages/detail/index.wxss
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
|
||||
.detail { display:flex;
|
||||
}
|
||||
.filter-tabs { display:flex; gap: 24rpx; padding: 18rpx 24rpx; color:#666;
|
||||
}
|
||||
.filter-tabs .tab.active { color:#2aa7b6; font-weight: 700;
|
||||
}
|
||||
.biz-tabs { position: fixed; left:0; top: 160rpx; bottom: 120rpx; width: 120rpx; display:flex; flex-direction: column; gap: 24rpx; padding: 12rpx;
|
||||
}
|
||||
.biz { background:#6aa9ff; color:#fff; border-radius: 16rpx; padding: 20rpx 0; text-align:center; opacity: .85;
|
||||
}
|
||||
.biz.active { opacity: 1;
|
||||
}
|
||||
.card { margin-left: 140rpx; background:#fff; border-radius: 24rpx; padding: 16rpx;
|
||||
}
|
||||
.subtabs { display:flex; align-items:center; gap: 24rpx; padding: 8rpx 6rpx 12rpx;
|
||||
}
|
||||
.sub { color:#57c2cf; padding: 8rpx 12rpx;
|
||||
}
|
||||
.sub.active { border-bottom: 4rpx solid #57c2cf; font-weight:700;
|
||||
}
|
||||
.plus { margin-left:auto; width: 60rpx; height: 60rpx; border-radius: 30rpx; background:#2ec0d0; color:#fff; font-size: 40rpx; display:flex; align-items:center; justify-content:center;
|
||||
}
|
||||
.search { background:#f6f7fb; border-radius: 999rpx; padding: 14rpx 20rpx; margin: 8rpx 0 12rpx;
|
||||
}
|
||||
.daterange { display:flex; align-items:center; gap: 12rpx; color:#888; padding-bottom: 8rpx;
|
||||
}
|
||||
.daterange .sep { color:#ccc;
|
||||
}
|
||||
.total { color:#2ec0d0; font-weight: 800; padding: 12rpx 0; border-top: 2rpx solid #eaeaea;
|
||||
}
|
||||
.list { height: calc(100vh - 420rpx);
|
||||
}
|
||||
.row { display:flex; justify-content: space-between; align-items:center; padding: 22rpx 10rpx; border-bottom: 1rpx solid #f0f0f0;
|
||||
}
|
||||
.left .date { color:#999; margin-bottom: 6rpx;
|
||||
}
|
||||
.left .name { color:#333; margin-bottom: 6rpx;
|
||||
}
|
||||
.left .no { color:#bbb;
|
||||
}
|
||||
.right { color:#555;
|
||||
}
|
||||
@@ -1,20 +1,17 @@
|
||||
"use strict";
|
||||
const common_vendor = require("../../common/vendor.js");
|
||||
const common_http = require("../../common/http.js");
|
||||
const common_assets = require("../../common/assets.js");
|
||||
const _sfc_main = {
|
||||
data() {
|
||||
return {
|
||||
todayAmount: "0.00",
|
||||
monthProfit: "0.00",
|
||||
stockQty: "0.00",
|
||||
kpi: { todaySales: "0.00", monthSales: "0.00", monthProfit: "0.00", stockCount: "0" },
|
||||
activeTab: "home",
|
||||
notices: [
|
||||
{ text: "选材精工:不锈钢、合金钢,耐腐蚀更耐用", tag: "品质" },
|
||||
{ text: "表面工艺:电镀锌/镀镍/发黑处理,性能均衡", tag: "工艺" },
|
||||
{ text: "库存齐全:螺丝、螺母、垫圈、膨胀螺栓等现货", tag: "库存" },
|
||||
{ text: "企业采购支持:批量优惠,次日发货", tag: "服务" }
|
||||
],
|
||||
notices: [],
|
||||
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: "💳" },
|
||||
@@ -27,35 +24,78 @@ const _sfc_main = {
|
||||
]
|
||||
};
|
||||
},
|
||||
onLoad() {
|
||||
this.fetchMetrics();
|
||||
this.fetchNotices();
|
||||
},
|
||||
methods: {
|
||||
async fetchMetrics() {
|
||||
try {
|
||||
const d = await common_http.get("/api/dashboard/overview");
|
||||
const toNum = (v) => typeof v === "number" ? v : Number(v || 0);
|
||||
this.kpi = {
|
||||
...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) {
|
||||
}
|
||||
},
|
||||
async fetchNotices() {
|
||||
this.loadingNotices = true;
|
||||
this.noticeError = "";
|
||||
try {
|
||||
const list = await common_http.get("/api/notices");
|
||||
this.notices = Array.isArray(list) ? list.map((n) => ({
|
||||
text: n.content || n.title || "",
|
||||
tag: n.tag || ""
|
||||
})) : [];
|
||||
} catch (e) {
|
||||
this.noticeError = e && e.message || "公告加载失败";
|
||||
} finally {
|
||||
this.loadingNotices = false;
|
||||
}
|
||||
},
|
||||
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.showToast({ title: "开单(开发中)", icon: "none" });
|
||||
common_vendor.index.navigateTo({ url: "/pages/order/create" });
|
||||
},
|
||||
onNoticeTap(n) {
|
||||
common_vendor.index.showModal({
|
||||
title: "公告",
|
||||
content: n.text,
|
||||
title: "广告",
|
||||
content: n && (n.text || n.title || n.content) || "",
|
||||
showCancel: false
|
||||
});
|
||||
},
|
||||
onNoticeList() {
|
||||
common_vendor.index.showToast({ title: "公告列表(开发中)", icon: "none" });
|
||||
},
|
||||
onIconError(item) {
|
||||
item.img = "";
|
||||
}
|
||||
}
|
||||
};
|
||||
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
|
||||
return {
|
||||
return common_vendor.e({
|
||||
a: common_assets._imports_0,
|
||||
b: common_vendor.t($data.todayAmount),
|
||||
c: common_vendor.t($data.monthProfit),
|
||||
d: common_vendor.t($data.stockQty),
|
||||
e: common_vendor.f($data.notices, (n, idx, i0) => {
|
||||
b: common_vendor.t($data.kpi.todaySales),
|
||||
c: common_vendor.t($data.kpi.monthSales),
|
||||
d: common_vendor.t($data.kpi.monthProfit),
|
||||
e: common_vendor.t($data.kpi.stockCount),
|
||||
f: $data.loadingNotices
|
||||
}, $data.loadingNotices ? {} : $data.noticeError ? {
|
||||
h: common_vendor.t($data.noticeError)
|
||||
} : !$data.notices.length ? {} : {
|
||||
j: common_vendor.f($data.notices, (n, idx, i0) => {
|
||||
return common_vendor.e({
|
||||
a: common_vendor.t(n.text),
|
||||
b: n.tag
|
||||
@@ -65,9 +105,11 @@ function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
|
||||
d: common_vendor.o(($event) => $options.onNoticeTap(n), idx),
|
||||
e: idx
|
||||
});
|
||||
}),
|
||||
f: common_vendor.o((...args) => $options.onNoticeList && $options.onNoticeList(...args)),
|
||||
g: common_vendor.f($data.features, (item, k0, i0) => {
|
||||
})
|
||||
}, {
|
||||
g: $data.noticeError,
|
||||
i: !$data.notices.length,
|
||||
k: common_vendor.f($data.features, (item, k0, i0) => {
|
||||
return common_vendor.e({
|
||||
a: item.img
|
||||
}, item.img ? {
|
||||
@@ -82,14 +124,18 @@ function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
|
||||
h: common_vendor.o(($event) => $options.onFeatureTap(item), item.key)
|
||||
});
|
||||
}),
|
||||
h: $data.activeTab === "home" ? 1 : "",
|
||||
i: common_vendor.o(($event) => $data.activeTab = "home"),
|
||||
j: common_vendor.o((...args) => $options.onCreateOrder && $options.onCreateOrder(...args)),
|
||||
k: $data.activeTab === "detail" ? 1 : "",
|
||||
l: common_vendor.o(($event) => $data.activeTab = "detail"),
|
||||
m: $data.activeTab === "me" ? 1 : "",
|
||||
n: common_vendor.o(($event) => $data.activeTab = "me")
|
||||
};
|
||||
l: $data.activeTab === "home" ? 1 : "",
|
||||
m: common_vendor.o(($event) => $data.activeTab = "home"),
|
||||
n: $data.activeTab === "product" ? 1 : "",
|
||||
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"),
|
||||
s: $data.activeTab === "report" ? 1 : "",
|
||||
t: common_vendor.o(($event) => $data.activeTab = "report"),
|
||||
v: $data.activeTab === "me" ? 1 : "",
|
||||
w: common_vendor.o(($event) => $data.activeTab = "me")
|
||||
});
|
||||
}
|
||||
const MiniProgramPage = /* @__PURE__ */ common_vendor._export_sfc(_sfc_main, [["render", _sfc_render]]);
|
||||
wx.createPage(MiniProgramPage);
|
||||
|
||||
@@ -1 +1 @@
|
||||
<view class="home"><image class="home-bg" src="{{a}}" mode="aspectFill"></image><view class="hero"><view class="hero-top"><text class="brand">五金配件管家</text><view class="cta"><text class="cta-text">咨询</text></view></view><view class="kpi"><view class="kpi-item"><text class="kpi-label">今日销售额</text><text class="kpi-value">{{b}}</text></view><view class="kpi-item"><text class="kpi-label">本月利润</text><text class="kpi-value">{{c}}</text></view><view class="kpi-item"><text class="kpi-label">库存数量</text><text class="kpi-value">{{d}}</text></view></view></view><view class="notice"><view class="notice-left">公告</view><swiper class="notice-swiper" circular autoplay interval="4000" duration="400" vertical><swiper-item wx:for="{{e}}" wx:for-item="n" wx:key="e"><view class="notice-item" bindtap="{{n.d}}"><text class="notice-text">{{n.a}}</text><text wx:if="{{n.b}}" class="notice-tag">{{n.c}}</text></view></swiper-item></swiper><view class="notice-right" bindtap="{{f}}">更多</view></view><view class="section-title"><text class="section-text">常用功能</text></view><view class="grid-wrap"><view class="grid"><view wx:for="{{g}}" wx:for-item="item" wx:key="g" class="grid-item" bindtap="{{item.h}}"><view class="icon icon-squircle"><image wx:if="{{item.a}}" src="{{item.b}}" class="icon-img" mode="aspectFit" binderror="{{item.c}}"></image><text wx:elif="{{item.d}}" class="icon-emoji">{{item.e}}</text><view wx:else class="icon-placeholder"></view></view><text class="grid-chip">{{item.f}}</text></view></view></view><view class="bottom-bar"><view class="{{['tab', h && 'active']}}" bindtap="{{i}}"><text>首页</text></view><view class="tab primary" bindtap="{{j}}"><text>开单</text></view><view class="{{['tab', k && 'active']}}" bindtap="{{l}}"><text>明细</text></view><view class="{{['tab', m && 'active']}}" bindtap="{{n}}"><text>我的</text></view></view></view>
|
||||
<view class="home"><image class="home-bg" src="{{a}}" mode="aspectFill"></image><view class="hero"><view class="hero-top"><text class="brand">五金配件管家</text><view class="cta"><text class="cta-text">咨询</text></view></view><view class="kpi"><view class="kpi-item"><text class="kpi-label">今日销售额</text><text class="kpi-value">{{b}}</text></view><view class="kpi-item"><text class="kpi-label">本月销售额</text><text class="kpi-value">{{c}}</text></view><view class="kpi-item"><text class="kpi-label">本月利润</text><text class="kpi-value">{{d}}</text></view><view class="kpi-item"><text class="kpi-label">库存商品数量</text><text class="kpi-value">{{e}}</text></view></view></view><view class="notice"><view class="notice-left">广告</view><view wx:if="{{f}}" class="notice-swiper" style="display:flex;align-items:center;color:#6b5a2a">加载中...</view><view wx:elif="{{g}}" class="notice-swiper" style="display:flex;align-items:center;color:#dd524d">{{h}}</view><view wx:elif="{{i}}" class="notice-swiper" style="display:flex;align-items:center;color:#6b5a2a">暂无公告</view><swiper wx:else class="notice-swiper" circular autoplay interval="4000" duration="400" vertical><swiper-item wx:for="{{j}}" wx:for-item="n" wx:key="e"><view class="notice-item" bindtap="{{n.d}}"><text class="notice-text">{{n.a}}</text><text wx:if="{{n.b}}" class="notice-tag">{{n.c}}</text></view></swiper-item></swiper></view><view class="section-title"><text class="section-text">常用功能</text></view><view class="grid-wrap"><view class="grid"><view wx:for="{{k}}" wx:for-item="item" wx:key="g" class="grid-item" bindtap="{{item.h}}"><view class="icon icon-squircle"><image wx:if="{{item.a}}" src="{{item.b}}" class="icon-img" mode="aspectFit" binderror="{{item.c}}"></image><text wx:elif="{{item.d}}" class="icon-emoji">{{item.e}}</text><view wx:else class="icon-placeholder"></view></view><text class="grid-chip">{{item.f}}</text></view></view></view><view class="bottom-bar"><view class="{{['tab', l && 'active']}}" bindtap="{{m}}"><text>首页</text></view><view class="{{['tab', n && 'active']}}" bindtap="{{o}}"><text>货品</text></view><view class="tab primary" bindtap="{{p}}"><text>开单</text></view><view class="{{['tab', q && 'active']}}" bindtap="{{r}}"><text>明细</text></view><view class="{{['tab', s && 'active']}}" bindtap="{{t}}"><text>报表</text></view><view class="{{['tab', v && 'active']}}" bindtap="{{w}}"><text>我的</text></view></view></view>
|
||||
@@ -48,8 +48,7 @@
|
||||
}
|
||||
.notice-tag { color: #B4880F; font-size: 22rpx; padding: 4rpx 10rpx; border-radius: 999rpx; background: rgba(215,167,46,0.18);
|
||||
}
|
||||
.notice-right { flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center; min-width: 72rpx; height: 44rpx; color: #B4880F; font-size: 26rpx; padding-left: 8rpx;
|
||||
}
|
||||
|
||||
|
||||
/* 分割标题 */
|
||||
.section-title { display: flex; align-items: center; gap: 16rpx; padding: 10rpx 28rpx 0;
|
||||
|
||||
212
frontend/unpackage/dist/dev/mp-weixin/pages/order/create.js
vendored
Normal file
212
frontend/unpackage/dist/dev/mp-weixin/pages/order/create.js
vendored
Normal file
@@ -0,0 +1,212 @@
|
||||
"use strict";
|
||||
const common_vendor = require("../../common/vendor.js");
|
||||
const common_http = require("../../common/http.js");
|
||||
const common_constants = require("../../common/constants.js");
|
||||
const common_assets = require("../../common/assets.js");
|
||||
function todayString() {
|
||||
const d = /* @__PURE__ */ new Date();
|
||||
const m = (d.getMonth() + 1).toString().padStart(2, "0");
|
||||
const day = d.getDate().toString().padStart(2, "0");
|
||||
return `${d.getFullYear()}-${m}-${day}`;
|
||||
}
|
||||
const _sfc_main = {
|
||||
data() {
|
||||
return {
|
||||
biz: "sale",
|
||||
saleType: "out",
|
||||
purchaseType: "in",
|
||||
order: {
|
||||
orderTime: todayString(),
|
||||
customerId: null,
|
||||
supplierId: null,
|
||||
remark: ""
|
||||
},
|
||||
customerName: "",
|
||||
supplierName: "",
|
||||
items: [],
|
||||
activeCategory: "sale_income",
|
||||
trxAmount: 0,
|
||||
selectedAccountId: null,
|
||||
selectedAccountName: ""
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
totalQuantity() {
|
||||
return this.items.reduce((s, it) => s + Number(it.quantity || 0), 0);
|
||||
},
|
||||
totalAmount() {
|
||||
return this.items.reduce((s, it) => s + Number(it.quantity || 0) * Number(it.unitPrice || 0), 0);
|
||||
},
|
||||
customerLabel() {
|
||||
return this.customerName || "零售客户";
|
||||
},
|
||||
supplierLabel() {
|
||||
return this.supplierName || "零散供应商";
|
||||
},
|
||||
incomeCategories() {
|
||||
return common_constants.INCOME_CATEGORIES;
|
||||
},
|
||||
expenseCategories() {
|
||||
return common_constants.EXPENSE_CATEGORIES;
|
||||
},
|
||||
accountLabel() {
|
||||
return this.selectedAccountName || "现金";
|
||||
},
|
||||
counterpartyLabel() {
|
||||
return this.customerName || this.supplierName || "—";
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
switchBiz(type) {
|
||||
this.biz = type;
|
||||
},
|
||||
onDateChange(e) {
|
||||
this.order.orderTime = e.detail.value;
|
||||
},
|
||||
chooseCustomer() {
|
||||
common_vendor.index.navigateTo({ url: "/pages/customer/select" });
|
||||
},
|
||||
chooseSupplier() {
|
||||
common_vendor.index.navigateTo({ url: "/pages/supplier/select" });
|
||||
},
|
||||
chooseProduct() {
|
||||
common_vendor.index.navigateTo({ url: "/pages/product/select" });
|
||||
},
|
||||
chooseAccount() {
|
||||
common_vendor.index.navigateTo({ url: "/pages/account/select" });
|
||||
},
|
||||
chooseCounterparty() {
|
||||
if (this.biz === "income" || this.biz === "expense") {
|
||||
common_vendor.index.navigateTo({ url: "/pages/customer/select" });
|
||||
}
|
||||
},
|
||||
recalc() {
|
||||
this.$forceUpdate();
|
||||
},
|
||||
async submit() {
|
||||
const isSaleOrPurchase = this.biz === "sale" || this.biz === "purchase";
|
||||
const payload = isSaleOrPurchase ? {
|
||||
type: this.biz === "sale" ? this.saleType : "purchase." + this.purchaseType,
|
||||
orderTime: this.order.orderTime,
|
||||
customerId: this.order.customerId,
|
||||
supplierId: this.order.supplierId,
|
||||
items: this.items.map((it) => ({ productId: it.productId, quantity: Number(it.quantity || 0), unitPrice: Number(it.unitPrice || 0) })),
|
||||
amount: this.totalAmount
|
||||
} : {
|
||||
type: this.biz,
|
||||
category: this.activeCategory,
|
||||
counterpartyId: this.order.customerId || null,
|
||||
accountId: this.selectedAccountId || null,
|
||||
amount: Number(this.trxAmount || 0),
|
||||
txTime: this.order.orderTime,
|
||||
remark: this.order.remark
|
||||
};
|
||||
try {
|
||||
const url = isSaleOrPurchase ? "/api/orders" : "/api/other-transactions";
|
||||
await common_http.post(url, 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" });
|
||||
}
|
||||
},
|
||||
saveAndReset() {
|
||||
this.items = [];
|
||||
this.trxAmount = 0;
|
||||
this.order.remark = "";
|
||||
}
|
||||
}
|
||||
};
|
||||
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
|
||||
return common_vendor.e({
|
||||
a: $data.biz === "sale" ? 1 : "",
|
||||
b: common_vendor.o(($event) => $options.switchBiz("sale")),
|
||||
c: $data.biz === "purchase" ? 1 : "",
|
||||
d: common_vendor.o(($event) => $options.switchBiz("purchase")),
|
||||
e: $data.biz === "income" ? 1 : "",
|
||||
f: common_vendor.o(($event) => $options.switchBiz("income")),
|
||||
g: $data.biz === "expense" ? 1 : "",
|
||||
h: common_vendor.o(($event) => $options.switchBiz("expense")),
|
||||
i: $data.biz === "sale"
|
||||
}, $data.biz === "sale" ? {
|
||||
j: $data.saleType === "out" ? 1 : "",
|
||||
k: common_vendor.o(($event) => $data.saleType = "out"),
|
||||
l: $data.saleType === "return" ? 1 : "",
|
||||
m: common_vendor.o(($event) => $data.saleType = "return"),
|
||||
n: $data.saleType === "collect" ? 1 : "",
|
||||
o: common_vendor.o(($event) => $data.saleType = "collect")
|
||||
} : $data.biz === "purchase" ? {
|
||||
q: $data.purchaseType === "in" ? 1 : "",
|
||||
r: common_vendor.o(($event) => $data.purchaseType = "in"),
|
||||
s: $data.purchaseType === "return" ? 1 : "",
|
||||
t: common_vendor.o(($event) => $data.purchaseType = "return"),
|
||||
v: $data.purchaseType === "pay" ? 1 : "",
|
||||
w: common_vendor.o(($event) => $data.purchaseType = "pay")
|
||||
} : {}, {
|
||||
p: $data.biz === "purchase",
|
||||
x: common_vendor.t($data.order.orderTime),
|
||||
y: $data.order.orderTime,
|
||||
z: common_vendor.o((...args) => $options.onDateChange && $options.onDateChange(...args)),
|
||||
A: $data.biz === "sale"
|
||||
}, $data.biz === "sale" ? {
|
||||
B: common_vendor.t($options.customerLabel),
|
||||
C: common_vendor.o((...args) => $options.chooseCustomer && $options.chooseCustomer(...args))
|
||||
} : $data.biz === "purchase" ? {
|
||||
E: common_vendor.t($options.supplierLabel),
|
||||
F: common_vendor.o((...args) => $options.chooseSupplier && $options.chooseSupplier(...args))
|
||||
} : {}, {
|
||||
D: $data.biz === "purchase",
|
||||
G: $data.biz === "sale" || $data.biz === "purchase"
|
||||
}, $data.biz === "sale" || $data.biz === "purchase" ? {
|
||||
H: common_vendor.t($options.totalQuantity),
|
||||
I: common_vendor.t($options.totalAmount.toFixed(2)),
|
||||
J: common_vendor.o((...args) => $options.chooseProduct && $options.chooseProduct(...args))
|
||||
} : {
|
||||
K: common_vendor.f($data.biz === "income" ? $options.incomeCategories : $options.expenseCategories, (c, k0, i0) => {
|
||||
return {
|
||||
a: common_vendor.t(c.label),
|
||||
b: c.key,
|
||||
c: $data.activeCategory === c.key ? 1 : "",
|
||||
d: common_vendor.o(($event) => $data.activeCategory = c.key, c.key)
|
||||
};
|
||||
}),
|
||||
L: common_vendor.t($options.counterpartyLabel),
|
||||
M: common_vendor.o((...args) => $options.chooseCounterparty && $options.chooseCounterparty(...args)),
|
||||
N: common_vendor.t($options.accountLabel),
|
||||
O: common_vendor.o((...args) => $options.chooseAccount && $options.chooseAccount(...args)),
|
||||
P: $data.trxAmount,
|
||||
Q: common_vendor.o(common_vendor.m(($event) => $data.trxAmount = $event.detail.value, {
|
||||
number: true
|
||||
})),
|
||||
R: $data.order.remark,
|
||||
S: common_vendor.o(($event) => $data.order.remark = $event.detail.value)
|
||||
}, {
|
||||
T: !$data.items.length
|
||||
}, !$data.items.length ? {
|
||||
U: common_assets._imports_0$1
|
||||
} : {
|
||||
V: common_vendor.f($data.items, (it, idx, i0) => {
|
||||
return {
|
||||
a: common_vendor.t(it.productName),
|
||||
b: common_vendor.o([common_vendor.m(($event) => it.quantity = $event.detail.value, {
|
||||
number: true
|
||||
}), ($event) => $options.recalc()], idx),
|
||||
c: it.quantity,
|
||||
d: common_vendor.o([common_vendor.m(($event) => it.unitPrice = $event.detail.value, {
|
||||
number: true
|
||||
}), ($event) => $options.recalc()], idx),
|
||||
e: it.unitPrice,
|
||||
f: common_vendor.t((Number(it.quantity) * Number(it.unitPrice)).toFixed(2)),
|
||||
g: idx
|
||||
};
|
||||
})
|
||||
}, {
|
||||
W: common_vendor.o((...args) => $options.saveAndReset && $options.saveAndReset(...args)),
|
||||
X: 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/order/create.js.map
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user