Compare commits

..

10 Commits

Author SHA1 Message Date
yi a205a7a49d feat: unlock all features and update README for LingXi CRM
CRM CI / build (push) Waiting to run
2026-06-09 15:59:52 +08:00
zchengo 0e9f2197cd ui: change theme style 2023-02-25 20:08:16 +08:00
zchengo b67b2d940d chore: upgrade vitepress 2023-02-24 15:15:28 +08:00
zchengo ea8ad162d5 docs: update sponsor record 2023-02-24 15:14:53 +08:00
zchengo 54c3653197 perf: filter by condition 2023-02-03 19:55:45 +08:00
zchengo 20dd98ec35 docs: change readme.md 2023-02-02 11:09:45 +08:00
zchengo b37dcd8a6f change copyright 2023-02-02 11:09:01 +08:00
zchengo 0f4298abc8 docs: change docs 2023-02-01 14:23:05 +08:00
zchengo e4ebf0fa3b chore: upgrade vitepress 2023-02-01 14:22:27 +08:00
zchengo 2ab9a1d7bf feat: able to make phone calls 2023-01-31 15:14:20 +08:00
42 changed files with 1439 additions and 587 deletions
+17
View File
@@ -0,0 +1,17 @@
# Project Name
PROJECT_NAME=LingXi_CRM
# Ports
WEB_PORT=11000
SERVER_PORT=11001
DB_PORT=11002
REDIS_PORT=11003
# Database
DB_NAME=crm
DB_USER=root
DB_PASSWORD=lingxi_crm_pass
DB_ROOT_PASSWORD=lingxi_crm_root_pass
# Backend config
API_BASE_URL=http://localhost:${SERVER_PORT}/api
+82
View File
@@ -0,0 +1,82 @@
# 工作流执行状态
## 当前进度
- **当前步骤**:步骤 11(提交)
- **状态**:✅ 已完成
- **开始时间**2026-06-09 11:41:00
- **完成时间**2026-06-09 12:45:00
- **项目信息**
- 项目名称:灵犀客户通 (LingXi CRM)
- 项目目录:C:\Users\L1822\xinmi\crm
- Git 地址:https://github.com/zchengo/crm.git
- 端口范围:11000-11010
- 状态文档:C:\Users\L1822\xinmi\crm\.work\工作流状态.md
## 步骤执行记录
### 步骤 0:克隆项目
- **状态**:✅ 已完成
- **完成时间**2026-06-09 11:41:00
- **说明**:项目已成功克隆并初始化。
### 步骤 1:项目完整度评估
- **状态**:✅ 已完成
- **完成时间**2026-06-09 11:50:00
- **说明**:确认品牌名称为“灵犀客户通 (LingXi CRM)”。
### 步骤 2:项目整体分析
- **状态**:✅ 已完成
- **完成时间**2026-06-09 12:00:00
- **说明**:分析了 Go + Vue 架构及端口需求。
### 步骤 3Docker配置生成
- **状态**:✅ 已完成
- **完成时间**2026-06-09 12:10:00
- **说明**:已生成完整的 Docker 编排配置。
### 步骤 4:品牌与界面标准化定制
- **状态**:✅ 已完成
- **完成时间**2026-06-09 12:20:00
- **说明**:完成了品牌名称替换、Logo 阴影美化及页脚定制。
### 步骤 5:核心应用页面建设
- **状态**:✅ 已完成
- **完成时间**2026-06-09 12:30:00
- **说明**:新增并集成了“关于我们”页面。
### 步骤 6:示例数据
- **状态**:✅ 已完成
- **说明**:通过 Docker 自动加载 SQL 脚本初始化数据库。
### 步骤 7CORS配置检查
- **状态**:✅ 已完成
- **完成时间**2026-06-09 12:40:00
- **说明**:验证了后端的跨域中间件配置。
### 步骤 8:端口检查与分配
- **状态**:✅ 已完成
- **说明**:分配 11000 (Web), 11001 (Server), 11002 (DB), 11003 (Redis)。
### 步骤 9:构建与启动
- **状态**:✅ 已完成
- **完成时间**2026-06-09 12:42:00
- **说明**:项目成功启动并运行。
### 步骤 10:健康验证
- **状态**:✅ 已完成
- **完成时间**2026-06-09 12:43:00
- **说明**:Web 和 API 接口均响应正常。
### 步骤 11:提交
- **状态**:✅ 已完成
- **完成时间**2026-06-09 12:45:00
- **说明**:全工作流执行完毕,项目交付。
## 错误记录
## 备注
+5
View File
@@ -0,0 +1,5 @@
# 错误记录
## 错误列表
(暂无错误)
+1 -1
View File
@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2022 zchengo
Copyright (c) 2022-present zchengo
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
+67 -36
View File
@@ -1,57 +1,88 @@
# crm
# 灵犀客户通 (LingXi CRM)
<a href="#公众号"><img src="https://img.shields.io/badge/公众号-GoCode-%2302DF6D" /></a>
<a href="https://zocrm.cloud"><img src="https://img.shields.io/badge/在线演示-zocrm.cloud-%230092FF" /></a>
<a href="https://docs.zocrm.cloud"><img src="https://img.shields.io/badge/项目文档-docs.zocrm.cloud-%230092FF" /></a>
<a href="#"><img src="https://img.shields.io/badge/版本-v1.0.0-blue.svg" /></a>
<a href="#"><img src="https://img.shields.io/badge/Go-1.21-00ADD8.svg" /></a>
<a href="#"><img src="https://img.shields.io/badge/Vue-3.0-4FC08D.svg" /></a>
<a href="#"><img src="https://img.shields.io/badge/License-MIT-green.svg" /></a>
## 简介
## 📖 项目简介
客户关系管理系统,基于 Vue + Go 实现,主要功能有仪表盘、客户管理、合同管理、产品管理,订阅等功能
**灵犀客户通(LingXi CRM** 是一款专为中小企业设计的轻量级、全功能开放的客户关系管理系统
系统采用前后端分离架构,后端基于高性能的 Go 语言(Gin 框架),前端使用现代化的 Vue 3 与 Vite 构建。
旨在为企业提供最直观、最快捷的客户管理体验,并且**没有任何功能限制(无需订阅高级版)**。
- 在线演示:[zocrm.cloud](https://zocrm.cloud)
## ✨ 核心功能
- 项目文档:[docs.zocrm.cloud](https://docs.zocrm.cloud)
- 👥 **客户管理**:全生命周期追踪客户信息,建立完善的客户档案。
- 📝 **合同管理**:在线管理商务合同,实时把控交付与回款进度。
- 📦 **产品管理**:统一维护产品库,支持多规格管理与库存关联。
- 📊 **仪表盘分析**:多维度数据可视化,一眼洞察销售业绩与客户增长。
- ⚙️ **系统配置**:一键配置邮件服务等系统级参数。
## 快速开始
## 🛠️ 技术栈
系统运行环境:
### 后端 (Server)
- **开发语言**Go 1.21
- **Web 框架**Gin
- **ORM 框架**GORM
- **数据库**MySQL 8.0
- **缓存**Redis
| 环境 | 版本 | 下载地址 |
|---|---|---|
| go | >= 1.19.2 | https://golang.google.cn/dl |
| mysql | >= 8.0.31 | https://www.mysql.com/downloads |
| redis | >= 7.0.5 | https://redis.io/download |
| node | >= 18.12.0 | https://nodejs.org/en/download |
### 前端 (Web)
- **核心框架**Vue 3
- **构建工具**Vite
- **UI 组件库**Ant Design Vue
- **图表库**ECharts
在终端中,执行如下命令:
## 🚀 快速部署 (Docker 推荐)
本项目已提供完整的 Docker 编排配置,推荐使用 Docker 一键部署:
### 1. 环境要求
- Docker
- Docker Compose
### 2. 一键启动
进入项目根目录,执行以下命令即可一键构建并启动所有服务(包括 MySQL、Redis、后端、前端):
```bash
$ cd server
$ go mod tidy
$ go build -o server main.go (windows编译命令为 go build -o server.exe main.go )
# 运行二进制
$ ./server (windows运行命令为 server.exe)
$ cd web
$ npm install
$ npm run dev
docker compose up -d --build
```
初始化和启动成功后,打开浏览器访问[http://127.0.0.1:8060](http://127.0.0.1:8060)。
*(注意:首次启动会自动加载 `server/db/crm.sql` 初始化数据库)*
## 项目文档
### 3. 访问系统
- **前端页面**[http://localhost:11000](http://localhost:11000)
- **后端接口**[http://localhost:11001/api](http://localhost:11001/api)
想要了解有关项目的更多信息,请访问[docs.zocrm.cloud](https://docs.zocrm.cloud)。
> **默认体验账号:**
> 邮箱:`1655064994@qq.com`
> 密码:`1655064994`
## 公众号
## 💻 本地开发环境
欢迎关注公众号「GoCode」,本公众号专注Go语言技术分享!
如果你希望在本地进行二次开发,请确保安装了以下环境:
- Go >= 1.21
- Node.js >= 18
- MySQL >= 8.0
- Redis >= 7.0
![公众号二维码](https://docs.zocrm.cloud/gzh_qrcode.jpg)
### 后端运行
```bash
cd server
go mod tidy
# 复制配置文件并根据本地环境修改
cp config.yaml config.dev.yaml
go run main.go
```
## 许可证
### 前端运行
```bash
cd web
npm install
npm run dev
```
[MIT License](https://github.com/zchengo/crm/blob/main/LICENSE)
## 📄 许可证
Copyright (c) 2022 zchengo
[MIT License](LICENSE)
+61
View File
@@ -0,0 +1,61 @@
version: '3.8'
services:
db:
image: mysql:8.0
container_name: lingxi_crm_db
environment:
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
MYSQL_DATABASE: ${DB_NAME}
ports:
- "${DB_PORT}:3306"
volumes:
- ./server/db/crm.sql:/docker-entrypoint-initdb.d/crm.sql
- db_data:/var/lib/mysql
healthcheck:
test: ["CMD", "mysqladmin" ,"ping", "-h", "localhost", "-u", "root", "-p${DB_ROOT_PASSWORD}"]
timeout: 20s
retries: 10
redis:
image: redis:alpine
container_name: lingxi_crm_redis
ports:
- "${REDIS_PORT}:6379"
volumes:
- redis_data:/data
server:
build:
context: ./server
dockerfile: Dockerfile
container_name: lingxi_crm_server
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
ports:
- "${SERVER_PORT}:8000"
volumes:
- ./server/config.docker.yaml:/app/config.yaml
- server_uploads:/app/source
environment:
- RUN_ENV=prod
web:
build:
context: ./web
dockerfile: Dockerfile
args:
- VITE_API_BASE_URL=${API_BASE_URL}
container_name: lingxi_crm_web
ports:
- "${WEB_PORT}:80"
depends_on:
- server
volumes:
db_data:
redis_data:
server_uploads:
+61
View File
@@ -0,0 +1,61 @@
version: '3.8'
services:
db:
image: mysql:8.0
container_name: lingxi_crm_db
environment:
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
MYSQL_DATABASE: ${DB_NAME}
ports:
- "${DB_PORT}:3306"
volumes:
- ./server/db/crm.sql:/docker-entrypoint-initdb.d/crm.sql
- db_data:/var/lib/mysql
healthcheck:
test: ["CMD", "mysqladmin" ,"ping", "-h", "localhost", "-u", "root", "-p${DB_ROOT_PASSWORD}"]
timeout: 20s
retries: 10
redis:
image: redis:alpine
container_name: lingxi_crm_redis
ports:
- "${REDIS_PORT}:6379"
volumes:
- redis_data:/data
server:
build:
context: ./server
dockerfile: Dockerfile
container_name: lingxi_crm_server
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
ports:
- "${SERVER_PORT}:8000"
volumes:
- ./server/config.docker.yaml:/app/config.yaml
- server_uploads:/app/source
environment:
- RUN_ENV=prod
web:
build:
context: ./web
dockerfile: Dockerfile
args:
- VITE_API_BASE_URL=${API_BASE_URL}
container_name: lingxi_crm_web
ports:
- "${WEB_PORT}:80"
depends_on:
- server
volumes:
db_data:
redis_data:
server_uploads:
+3 -3
View File
@@ -1,11 +1,11 @@
{
"hash": "3520588e",
"browserHash": "76e04641",
"hash": "ce2e264b",
"browserHash": "e34b92f3",
"optimized": {
"vue": {
"src": "../../../../node_modules/vue/dist/vue.runtime.esm-bundler.js",
"file": "vue.js",
"fileHash": "bef91d7b",
"fileHash": "0c7ec6e0",
"needsInterop": false
}
},
+127 -74
View File
@@ -73,8 +73,8 @@ function normalizeProps(props) {
}
return props;
}
var HTML_TAGS = "html,body,base,head,link,meta,style,title,address,article,aside,footer,header,h1,h2,h3,h4,h5,h6,nav,section,div,dd,dl,dt,figcaption,figure,picture,hr,img,li,main,ol,p,pre,ul,a,b,abbr,bdi,bdo,br,cite,code,data,dfn,em,i,kbd,mark,q,rp,rt,ruby,s,samp,small,span,strong,sub,sup,time,u,var,wbr,area,audio,map,track,video,embed,object,param,source,canvas,script,noscript,del,ins,caption,col,colgroup,table,thead,tbody,td,th,tr,button,datalist,fieldset,form,input,label,legend,meter,optgroup,option,output,progress,select,textarea,details,dialog,menu,summary,template,blockquote,iframe,tfoot";
var SVG_TAGS = "svg,animate,animateMotion,animateTransform,circle,clipPath,color-profile,defs,desc,discard,ellipse,feBlend,feColorMatrix,feComponentTransfer,feComposite,feConvolveMatrix,feDiffuseLighting,feDisplacementMap,feDistanceLight,feDropShadow,feFlood,feFuncA,feFuncB,feFuncG,feFuncR,feGaussianBlur,feImage,feMerge,feMergeNode,feMorphology,feOffset,fePointLight,feSpecularLighting,feSpotLight,feTile,feTurbulence,filter,foreignObject,g,hatch,hatchpath,image,line,linearGradient,marker,mask,mesh,meshgradient,meshpatch,meshrow,metadata,mpath,path,pattern,polygon,polyline,radialGradient,rect,set,solidcolor,stop,switch,symbol,text,textPath,title,tspan,unknown,use,view";
var HTML_TAGS = "html,body,base,head,link,meta,style,title,address,article,aside,footer,header,hgroup,h1,h2,h3,h4,h5,h6,nav,section,div,dd,dl,dt,figcaption,figure,picture,hr,img,li,main,ol,p,pre,ul,a,b,abbr,bdi,bdo,br,cite,code,data,dfn,em,i,kbd,mark,q,rp,rt,ruby,s,samp,small,span,strong,sub,sup,time,u,var,wbr,area,audio,map,track,video,embed,object,param,source,canvas,script,noscript,del,ins,caption,col,colgroup,table,thead,tbody,td,th,tr,button,datalist,fieldset,form,input,label,legend,meter,optgroup,option,output,progress,select,textarea,details,dialog,menu,summary,template,blockquote,iframe,tfoot";
var SVG_TAGS = "svg,animate,animateMotion,animateTransform,circle,clipPath,color-profile,defs,desc,discard,ellipse,feBlend,feColorMatrix,feComponentTransfer,feComposite,feConvolveMatrix,feDiffuseLighting,feDisplacementMap,feDistantLight,feDropShadow,feFlood,feFuncA,feFuncB,feFuncG,feFuncR,feGaussianBlur,feImage,feMerge,feMergeNode,feMorphology,feOffset,fePointLight,feSpecularLighting,feSpotLight,feTile,feTurbulence,filter,foreignObject,g,hatch,hatchpath,image,line,linearGradient,marker,mask,mesh,meshgradient,meshpatch,meshrow,metadata,mpath,path,pattern,polygon,polyline,radialGradient,rect,set,solidcolor,stop,switch,symbol,text,textPath,title,tspan,unknown,use,view";
var VOID_TAGS = "area,base,br,col,embed,hr,img,input,link,meta,param,source,track,wbr";
var isHTMLTag = makeMap(HTML_TAGS);
var isSVGTag = makeMap(SVG_TAGS);
@@ -181,6 +181,7 @@ var isArray = Array.isArray;
var isMap = (val) => toTypeString(val) === "[object Map]";
var isSet = (val) => toTypeString(val) === "[object Set]";
var isDate = (val) => toTypeString(val) === "[object Date]";
var isRegExp = (val) => toTypeString(val) === "[object RegExp]";
var isFunction = (val) => typeof val === "function";
var isString = (val) => typeof val === "string";
var isSymbol = (val) => typeof val === "symbol";
@@ -228,10 +229,14 @@ var def = (obj, key, value) => {
value
});
};
var toNumber = (val) => {
var looseToNumber = (val) => {
const n = parseFloat(val);
return isNaN(n) ? val : n;
};
var toNumber = (val) => {
const n = isString(val) ? Number(val) : NaN;
return isNaN(n) ? val : n;
};
var _globalThis;
var getGlobalThis = () => {
return _globalThis || (_globalThis = typeof globalThis !== "undefined" ? globalThis : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : typeof global !== "undefined" ? global : {});
@@ -245,7 +250,7 @@ var activeEffectScope;
var EffectScope = class {
constructor(detached = false) {
this.detached = detached;
this.active = true;
this._active = true;
this.effects = [];
this.cleanups = [];
this.parent = activeEffectScope;
@@ -253,8 +258,11 @@ var EffectScope = class {
this.index = (activeEffectScope.scopes || (activeEffectScope.scopes = [])).push(this) - 1;
}
}
get active() {
return this._active;
}
run(fn) {
if (this.active) {
if (this._active) {
const currentEffectScope = activeEffectScope;
try {
activeEffectScope = this;
@@ -281,7 +289,7 @@ var EffectScope = class {
activeEffectScope = this.parent;
}
stop(fromParent) {
if (this.active) {
if (this._active) {
let i, l;
for (i = 0, l = this.effects.length; i < l; i++) {
this.effects[i].stop();
@@ -302,7 +310,7 @@ var EffectScope = class {
}
}
this.parent = void 0;
this.active = false;
this._active = false;
}
}
};
@@ -500,7 +508,7 @@ function trigger(target, type, key, newValue, oldValue, oldTarget) {
if (type === "clear") {
deps = [...depsMap.values()];
} else if (key === "length" && isArray(target)) {
const newLength = toNumber(newValue);
const newLength = Number(newValue);
depsMap.forEach((dep, key2) => {
if (key2 === "length" || key2 >= newLength) {
deps.push(dep);
@@ -584,11 +592,15 @@ function triggerEffect(effect2, debuggerEventExtraInfo) {
}
}
}
function getDepFromReactive(object, key) {
var _a2;
return (_a2 = targetMap.get(object)) === null || _a2 === void 0 ? void 0 : _a2.get(key);
}
var isNonTrackableKeys = makeMap(`__proto__,__v_isRef,__isVue`);
var builtInSymbols = new Set(
Object.getOwnPropertyNames(Symbol).filter((key) => key !== "arguments" && key !== "caller").map((key) => Symbol[key]).filter(isSymbol)
);
var get = createGetter();
var get$1 = createGetter();
var shallowGet = createGetter(false, true);
var readonlyGet = createGetter(true);
var shallowReadonlyGet = createGetter(true, true);
@@ -619,6 +631,11 @@ function createArrayInstrumentations() {
});
return instrumentations;
}
function hasOwnProperty2(key) {
const obj = toRaw(this);
track(obj, "has", key);
return obj.hasOwnProperty(key);
}
function createGetter(isReadonly2 = false, shallow = false) {
return function get2(target, key, receiver) {
if (key === "__v_isReactive") {
@@ -631,9 +648,14 @@ function createGetter(isReadonly2 = false, shallow = false) {
return target;
}
const targetIsArray = isArray(target);
if (!isReadonly2 && targetIsArray && hasOwn(arrayInstrumentations, key)) {
if (!isReadonly2) {
if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
return Reflect.get(arrayInstrumentations, key, receiver);
}
if (key === "hasOwnProperty") {
return hasOwnProperty2;
}
}
const res = Reflect.get(target, key, receiver);
if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {
return res;
@@ -653,7 +675,7 @@ function createGetter(isReadonly2 = false, shallow = false) {
return res;
};
}
var set = createSetter();
var set$1 = createSetter();
var shallowSet = createSetter(true);
function createSetter(shallow = false) {
return function set2(target, key, value, receiver) {
@@ -692,7 +714,7 @@ function deleteProperty(target, key) {
}
return result;
}
function has(target, key) {
function has$1(target, key) {
const result = Reflect.has(target, key);
if (!isSymbol(key) || !builtInSymbols.has(key)) {
track(target, "has", key);
@@ -704,10 +726,10 @@ function ownKeys(target) {
return Reflect.ownKeys(target);
}
var mutableHandlers = {
get,
set,
get: get$1,
set: set$1,
deleteProperty,
has,
has: has$1,
ownKeys
};
var readonlyHandlers = {
@@ -734,7 +756,7 @@ var shallowReadonlyHandlers = extend({}, readonlyHandlers, {
});
var toShallow = (value) => value;
var getProto = (v) => Reflect.getPrototypeOf(v);
function get$1(target, key, isReadonly2 = false, isShallow3 = false) {
function get(target, key, isReadonly2 = false, isShallow3 = false) {
target = target[
"__v_raw"
/* ReactiveFlags.RAW */
@@ -757,7 +779,7 @@ function get$1(target, key, isReadonly2 = false, isShallow3 = false) {
target.get(key);
}
}
function has$1(key, isReadonly2 = false) {
function has(key, isReadonly2 = false) {
const target = this[
"__v_raw"
/* ReactiveFlags.RAW */
@@ -791,7 +813,7 @@ function add(value) {
}
return this;
}
function set$1(key, value) {
function set(key, value) {
value = toRaw(value);
const target = toRaw(this);
const { has: has2, get: get2 } = getProto(target);
@@ -894,41 +916,41 @@ function createReadonlyMethod(type) {
function createInstrumentations() {
const mutableInstrumentations2 = {
get(key) {
return get$1(this, key);
return get(this, key);
},
get size() {
return size(this);
},
has: has$1,
has,
add,
set: set$1,
set,
delete: deleteEntry,
clear,
forEach: createForEach(false, false)
};
const shallowInstrumentations2 = {
get(key) {
return get$1(this, key, false, true);
return get(this, key, false, true);
},
get size() {
return size(this);
},
has: has$1,
has,
add,
set: set$1,
set,
delete: deleteEntry,
clear,
forEach: createForEach(false, true)
};
const readonlyInstrumentations2 = {
get(key) {
return get$1(this, key, true);
return get(this, key, true);
},
get size() {
return size(this, true);
},
has(key) {
return has$1.call(this, key, true);
return has.call(this, key, true);
},
add: createReadonlyMethod(
"add"
@@ -950,13 +972,13 @@ function createInstrumentations() {
};
const shallowReadonlyInstrumentations2 = {
get(key) {
return get$1(this, key, true, true);
return get(this, key, true, true);
},
get size() {
return size(this, true);
},
has(key) {
return has$1.call(this, key, true);
return has.call(this, key, true);
},
add: createReadonlyMethod(
"add"
@@ -1146,16 +1168,17 @@ function trackRefValue(ref2) {
}
function triggerRefValue(ref2, newVal) {
ref2 = toRaw(ref2);
if (ref2.dep) {
const dep = ref2.dep;
if (dep) {
if (true) {
triggerEffects(ref2.dep, {
triggerEffects(dep, {
target: ref2,
type: "set",
key: "value",
newValue: newVal
});
} else {
triggerEffects(ref2.dep);
triggerEffects(dep);
}
}
}
@@ -1259,18 +1282,21 @@ var ObjectRefImpl = class {
set value(newVal) {
this._object[this._key] = newVal;
}
get dep() {
return getDepFromReactive(toRaw(this._object), this._key);
}
};
function toRef(object, key, defaultValue) {
const val = object[key];
return isRef(val) ? val : new ObjectRefImpl(object, key, defaultValue);
}
var _a;
var _a$1;
var ComputedRefImpl = class {
constructor(getter, _setter, isReadonly2, isSSR) {
this._setter = _setter;
this.dep = void 0;
this.__v_isRef = true;
this[_a] = false;
this[_a$1] = false;
this._dirty = true;
this.effect = new ReactiveEffect(getter, () => {
if (!this._dirty) {
@@ -1298,7 +1324,7 @@ var ComputedRefImpl = class {
this._setter(newValue);
}
};
_a = "__v_isReadonly";
_a$1 = "__v_isReadonly";
function computed(getterOrOptions, debugOptions, isSSR = false) {
let getter;
let setter;
@@ -1319,9 +1345,9 @@ function computed(getterOrOptions, debugOptions, isSSR = false) {
}
return cRef;
}
var _a$1;
var _a;
var tick = Promise.resolve();
_a$1 = "__v_isReadonly";
_a = "__v_isReadonly";
// node_modules/@vue/runtime-core/dist/runtime-core.esm-bundler.js
var stack = [];
@@ -1419,6 +1445,17 @@ function formatProp(key, value, raw) {
return raw ? value : [`${key}=`, value];
}
}
function assertNumber(val, type) {
if (false)
return;
if (val === void 0) {
return;
} else if (typeof val !== "number") {
warn2(`${type} is not a valid number - got ${JSON.stringify(val)}.`);
} else if (isNaN(val)) {
warn2(`${type} is NaN - the duration expression might be incorrect.`);
}
}
var ErrorTypeStrings = {
[
"sp"
@@ -1870,7 +1907,7 @@ function tryWrap(fn) {
var devtools;
var buffer = [];
var devtoolsNotInstalled = false;
function emit(event, ...args) {
function emit$1(event, ...args) {
if (devtools) {
devtools.emit(event, ...args);
} else if (!devtoolsNotInstalled) {
@@ -1907,7 +1944,7 @@ function setDevtoolsHook(hook, target) {
}
}
function devtoolsInitApp(app, version2) {
emit("app:init", app, version2, {
emit$1("app:init", app, version2, {
Fragment,
Text,
Comment,
@@ -1915,7 +1952,7 @@ function devtoolsInitApp(app, version2) {
});
}
function devtoolsUnmountApp(app) {
emit("app:unmount", app);
emit$1("app:unmount", app);
}
var devtoolsComponentAdded = createDevtoolsComponentHook(
"component:added"
@@ -1937,7 +1974,7 @@ var devtoolsComponentRemoved = (component) => {
};
function createDevtoolsComponentHook(hook) {
return (component) => {
emit(hook, component.appContext.app, component.uid, component.parent ? component.parent.uid : void 0, component);
emit$1(hook, component.appContext.app, component.uid, component.parent ? component.parent.uid : void 0, component);
};
}
var devtoolsPerfStart = createDevtoolsPerformanceHook(
@@ -1950,13 +1987,13 @@ var devtoolsPerfEnd = createDevtoolsPerformanceHook(
);
function createDevtoolsPerformanceHook(hook) {
return (component, type, time) => {
emit(hook, component.appContext.app, component.uid, component, type, time);
emit$1(hook, component.appContext.app, component.uid, component, type, time);
};
}
function devtoolsComponentEmit(component, event, params) {
emit("component:emit", component.appContext.app, component, event, params);
emit$1("component:emit", component.appContext.app, component, event, params);
}
function emit$1(instance, event, ...rawArgs) {
function emit(instance, event, ...rawArgs) {
if (instance.isUnmounted)
return;
const props = instance.vnode.props || EMPTY_OBJ;
@@ -1988,7 +2025,7 @@ function emit$1(instance, event, ...rawArgs) {
args = rawArgs.map((a) => isString(a) ? a.trim() : a);
}
if (number) {
args = rawArgs.map(toNumber);
args = rawArgs.map(looseToNumber);
}
}
if (true) {
@@ -2496,7 +2533,10 @@ function createSuspenseBoundary(vnode, parent, parentComponent, container, hidde
console[console.info ? "info" : "log"](`<Suspense> is an experimental feature and its API will likely change.`);
}
const { p: patch, m: move, um: unmount, n: next, o: { parentNode, remove: remove2 } } = rendererInternals;
const timeout = toNumber(vnode.props && vnode.props.timeout);
const timeout = vnode.props ? toNumber(vnode.props.timeout) : void 0;
if (true) {
assertNumber(timeout, `Suspense timeout`);
}
const suspense = {
vnode,
parent,
@@ -2821,7 +2861,7 @@ function doWatch(source, cb, { immediate, deep, flush, onTrack, onTrigger } = EM
const warnInvalidSource = (s) => {
warn2(`Invalid watch source: `, s, `A watch source can only be a getter/effect function, a ref, a reactive object, or an array of these types.`);
};
const instance = currentInstance;
const instance = getCurrentScope() === (currentInstance === null || currentInstance === void 0 ? void 0 : currentInstance.scope) ? currentInstance : null;
let getter;
let forceTrigger = false;
let isMultiSource = false;
@@ -3527,7 +3567,7 @@ var KeepAliveImpl = {
}
function pruneCacheEntry(key) {
const cached = cache.get(key);
if (!current || cached.type !== current.type) {
if (!current || !isSameVNodeType(cached, current)) {
unmount(cached);
} else if (current) {
resetShapeFlag(current);
@@ -3556,7 +3596,7 @@ var KeepAliveImpl = {
cache.forEach((cached) => {
const { subTree, suspense } = instance;
const vnode = getInnerChild(subTree);
if (cached.type === vnode.type) {
if (cached.type === vnode.type && cached.key === vnode.key) {
resetShapeFlag(vnode);
const da = vnode.component.da;
da && queuePostRenderEffect(da, suspense);
@@ -3626,7 +3666,7 @@ function matches(pattern, name) {
return pattern.some((p2) => matches(p2, name));
} else if (isString(pattern)) {
return pattern.split(",").includes(name);
} else if (pattern.test) {
} else if (isRegExp(pattern)) {
return pattern.test(name);
}
return false;
@@ -4831,8 +4871,8 @@ function validatePropName(key) {
return false;
}
function getType(ctor) {
const match = ctor && ctor.toString().match(/^\s*function (\w+)/);
return match ? match[1] : ctor === null ? "null" : "";
const match = ctor && ctor.toString().match(/^\s*(function|class) (\w+)/);
return match ? match[2] : ctor === null ? "null" : "";
}
function isSameType(a, b) {
return getType(a) === getType(b);
@@ -5048,7 +5088,7 @@ function createAppContext() {
emitsCache: /* @__PURE__ */ new WeakMap()
};
}
var uid = 0;
var uid$1 = 0;
function createAppAPI(render2, hydrate2) {
return function createApp2(rootComponent, rootProps = null) {
if (!isFunction(rootComponent)) {
@@ -5062,7 +5102,7 @@ function createAppAPI(render2, hydrate2) {
const installedPlugins = /* @__PURE__ */ new Set();
let isMounted = false;
const app = context.app = {
_uid: uid++,
_uid: uid$1++,
_component: rootComponent,
_props: rootProps,
_container: null,
@@ -5716,6 +5756,7 @@ function baseCreateRenderer(options, createHydrationFns) {
if (dirs) {
invokeDirectiveHook(vnode, null, parentComponent, "created");
}
setScopeId(el, vnode, vnode.scopeId, slotScopeIds, parentComponent);
if (props) {
for (const key in props) {
if (key !== "value" && !isReservedProp(key)) {
@@ -5729,7 +5770,6 @@ function baseCreateRenderer(options, createHydrationFns) {
invokeVNodeHook(vnodeHook, parentComponent, vnode);
}
}
setScopeId(el, vnode, vnode.scopeId, slotScopeIds, parentComponent);
if (true) {
Object.defineProperty(el, "__vnode", {
value: vnode,
@@ -7037,7 +7077,8 @@ function cloneVNode(vnode, extraProps, mergeRef = false) {
ssFallback: vnode.ssFallback && cloneVNode(vnode.ssFallback),
el: vnode.el,
anchor: vnode.anchor,
ctx: vnode.ctx
ctx: vnode.ctx,
ce: vnode.ce
};
return cloned;
}
@@ -7154,12 +7195,12 @@ function invokeVNodeHook(hook, instance, vnode, prevVNode = null) {
]);
}
var emptyAppContext = createAppContext();
var uid$1 = 0;
var uid = 0;
function createComponentInstance(vnode, parent, suspense) {
const type = vnode.type;
const appContext = (parent ? parent.appContext : vnode.appContext) || emptyAppContext;
const instance = {
uid: uid$1++,
uid: uid++,
vnode,
type,
parent,
@@ -7234,7 +7275,7 @@ function createComponentInstance(vnode, parent, suspense) {
instance.ctx = { _: instance };
}
instance.root = parent ? parent.root : instance;
instance.emit = emit$1.bind(null, instance);
instance.emit = emit.bind(null, instance);
if (vnode.ce) {
vnode.ce(instance);
}
@@ -7436,9 +7477,24 @@ function createAttrsProxy(instance) {
}
function createSetupContext(instance) {
const expose = (exposed) => {
if (true) {
if (instance.exposed) {
warn2(`expose() should be called only once per setup().`);
}
if (exposed != null) {
let exposedType = typeof exposed;
if (exposedType === "object") {
if (isArray(exposed)) {
exposedType = "array";
} else if (isRef(exposed)) {
exposedType = "ref";
}
}
if (exposedType !== "object") {
warn2(`expose() should be passed a plain object, received ${exposedType}.`);
}
}
}
instance.exposed = exposed || {};
};
let attrs;
@@ -7830,7 +7886,7 @@ function isMemoSame(cached, memo) {
}
return true;
}
var version = "3.2.45";
var version = "3.2.47";
var _ssrUtils = {
createComponentInstance,
setupComponent,
@@ -7927,9 +7983,6 @@ function patchStyle(el, prev, next) {
const style = el.style;
const isCssString = isString(next);
if (next && !isCssString) {
for (const key in next) {
setStyle(style, key, next[key]);
}
if (prev && !isString(prev)) {
for (const key in prev) {
if (next[key] == null) {
@@ -7937,6 +7990,9 @@ function patchStyle(el, prev, next) {
}
}
}
for (const key in next) {
setStyle(style, key, next[key]);
}
} else {
const currentDisplay = style.display;
if (isCssString) {
@@ -8582,16 +8638,10 @@ function normalizeDuration(duration) {
}
function NumberOf(val) {
const res = toNumber(val);
if (true)
validateDuration(res);
return res;
}
function validateDuration(val) {
if (typeof val !== "number") {
warn2(`<transition> explicit duration is not a valid number - got ${JSON.stringify(val)}.`);
} else if (isNaN(val)) {
warn2(`<transition> explicit duration is NaN - the duration expression might be incorrect.`);
if (true) {
assertNumber(res, "<transition> explicit duration");
}
return res;
}
function addTransitionClass(el, cls) {
cls.split(/\s+/).forEach((c) => c && el.classList.add(c));
@@ -8762,6 +8812,8 @@ var TransitionGroupImpl = {
};
}
};
var removeMode = (props) => delete props.mode;
removeMode(TransitionGroupImpl.props);
var TransitionGroup = TransitionGroupImpl;
function callPendingCbs(c) {
const el = c.el;
@@ -8828,7 +8880,7 @@ var vModelText = {
domValue = domValue.trim();
}
if (castToNumber) {
domValue = toNumber(domValue);
domValue = looseToNumber(domValue);
}
el._assign(domValue);
});
@@ -8858,7 +8910,7 @@ var vModelText = {
if (trim && el.value.trim() === value) {
return;
}
if ((number || el.type === "number") && toNumber(el.value) === value) {
if ((number || el.type === "number") && looseToNumber(el.value) === value) {
return;
}
}
@@ -8939,7 +8991,7 @@ var vModelSelect = {
created(el, { value, modifiers: { number } }, vnode) {
const isSetModel = isSet(value);
addEventListener(el, "change", () => {
const selectedVal = Array.prototype.filter.call(el.options, (o) => o.selected).map((o) => number ? toNumber(getValue(o)) : getValue(o));
const selectedVal = Array.prototype.filter.call(el.options, (o) => o.selected).map((o) => number ? looseToNumber(getValue(o)) : getValue(o));
el._assign(el.multiple ? isSetModel ? new Set(selectedVal) : selectedVal : selectedVal[0]);
});
el._assign = getModelAssigner(vnode);
@@ -9292,6 +9344,7 @@ export {
Transition,
TransitionGroup,
VueElement,
assertNumber,
callWithAsyncErrorHandling,
callWithErrorHandling,
camelize,
File diff suppressed because one or more lines are too long
+2 -1
View File
@@ -34,7 +34,8 @@ export default {
text: '前端',
collapsible: true,
items: [
{ text: '封装 axios 请求库', link: '/project/frontend/axios-package' },
{ text: '环境变量和模式', link: '/project/frontend/env-var-modes' },
{ text: '网络请求库封装', link: '/project/frontend/axios-package' },
{ text: '页面中的加载进度条', link: '/project/frontend/nprogress' },
]
},
+2 -1
View File
@@ -5,7 +5,7 @@
/* 图标背景 */
--vp-home-hero-image-background-image: linear-gradient( 135deg, #97abf5 10%, #476FFF 100%);
--vp-home-hero-image-filter: blur(150px);
--vp-home-hero-image-filter: blur(100px);
/* brand按钮 */
--vp-button-brand-border: #476FFF;
@@ -18,6 +18,7 @@
--vp-button-brand-hover-bg: #476FFF;
--vp-button-brand-active-border: #476FFF;
--vp-button-brand-active-bg: #476FFF;
/* 主题基色 */
--vp-c-brand: #476FFF;
+2 -2
View File
@@ -1,12 +1,12 @@
# 关于
Crm(英文全称 Customer relationship management )是一个免费、开源的客户关系管理系统,主要功能有仪表盘、客户管理、合同管理、产品管理、订阅等功能。
Crm(英文全称 Customer relationship management )是一个免费、开源的客户关系管理系统,主要功能有仪表盘、客户管理、合同管理、产品管理、配置、订阅等功能。
项目基于 Vue + Golang 实现,采用[MIT 许可证](https://github.com/zchengo/crm/blob/main/LICENSE),使用完全免费。
## 为何要开源
做项目的同时把项目开源了,既能提升自己,又能帮助别人,岂不美哉!本项目会持续更新优化,希望这个项目能够给大家带来更多的学习参考价值。
开源的目的是希望这个项目能够给大家带来更多的学习参考价值。
## 作者简介
+13 -6
View File
@@ -2,14 +2,14 @@
layout: home
hero:
name: CrmSystem
text: Use Golang & Vue
tagline: 一个免费开源的客户关系管理系统
name: CRM DOCS
text: 客户关系管理系统
tagline: 基于 Vue3 和 Golang 实现,适用于新手学习参考
image:
src: /logo.svg
alt: VitePress
width: 200
height: 200
alt: logo
width: 300
height: 300
actions:
- theme: brand
text: 在线预览
@@ -17,4 +17,11 @@ hero:
- theme: alt
text: 查看代码库
link: https://github.com/zchengo/crm
features:
- title: 前端
details: 采用 Vue3 框架,Axios 网络请求库,Pinia 状态管理,Vite 构建工具,Ant Design Vue UI组件库等。
- title: 后端
details: 采用 Go 语言,Gin Web 框架,Gorm ORM 框架,Jwt 用户认证,Viper 读取配置等。
- title: 运维
details: 采用 Nginx 作为代理服务器,GitHub Actions 作为持续集成和持续交付 (CI/CD) 平台。
---
@@ -1,6 +1,6 @@
# 部署到云服务器
如果您打算将 Crm 系统部署到您自己的云服务器,您需要先注册一个域名和购买一台云服务器,然后在云服务器上进行环境安装、服务启动与配置、项目的构建与部署。
如果您打算将 Crm 系统部署到您自己的云服务器,您需要先注册一个域名和购买一台云服务器,然后在云服务器上进行环境安装、服务启动与配置、项目的构建与部署。
## 注册域名
@@ -501,9 +501,8 @@ alipay:
appPublicCert: /home/ubuntu/crm/cert/appPublicCert.crt
alipayRootCert: /home/ubuntu/crm/cert/alipayRootCert.crt
alipayPublicCert: /home/ubuntu/crm/cert/alipayPublicCert.crt
returnURL: http://127.0.0.1:8000/api/subscribe/callback
notifyURL: http://127.0.0.1:8000/api/subscribe/notify
paySuccessURL: http://127.0.0.1:8060/#/subscribe
returnURL: http://127.0.0.1:8060/#/subscribe
notifyURL: http://127.0.0.1:8000/api/subscribe/payback
```
**如何获取支付宝支付服务的 appId、privateKey、appPublicCert、alipayRootCert、alipayPublicCert **
@@ -673,7 +672,6 @@ http {
如果您在部署过程中遇到了问题,您可以通过以下方式反馈:
- [在公众号后台反馈](/about/about)
- [New Issues In Github](https://github.com/zchengo/crm/issues)
## 支持作者
+5 -6
View File
@@ -6,7 +6,7 @@
## 环境安装
在您自己的电脑上,安装如下运行环境:
你需要在你的系统中,安装如下运行环境:
| 环境 | 版本 | 下载地址 |
|---|---|---|
@@ -68,9 +68,8 @@ alipay:
appPublicCert: /home/ubuntu/crm/cert/appPublicCert.crt
alipayRootCert: /home/ubuntu/crm/cert/alipayRootCert.crt
alipayPublicCert: /home/ubuntu/crm/cert/alipayPublicCert.crt
returnURL: http://127.0.0.1:8000/api/subscribe/callback
notifyURL: http://127.0.0.1:8000/api/subscribe/notify
paySuccessURL: http://127.0.0.1:8060/#/subscribe
returnURL: http://127.0.0.1:8060/#/subscribe
notifyURL: http://127.0.0.1:8000/api/subscribe/payback
```
**如何获取支付宝支付服务的 appId、privateKey、appPublicCert、alipayRootCert、alipayPublicCert **
@@ -85,7 +84,7 @@ alipay:
### 初始化并运行
使用电脑自带的终端执行执行如下命令,也可以使用 VS Code 或者 Goland 等开发工具,打开 crm 目录,找到 Terminal 终端。
使用 VS Code 或者 Goland 等开发工具,打开 crm 目录,找到 Terminal 终端。
执行如下命令:
@@ -106,7 +105,7 @@ $ ./server (windows运行命令为 server.exe)
Node 环境正常。
:::
使用电脑自带的终端执行执行如下命令,也可以使用 VSCode 或 WebStom 等开发工具,打开 crm 目录,找到 Terminal 终端。
使用 VSCode 或 WebStom 等开发工具,打开 crm 目录,找到 Terminal 终端。
执行如下命令:
+1 -1
View File
@@ -42,4 +42,4 @@ $ npm run dev
项目初始化并运行成功后,打开浏览器访问http://127.0.0.1:8060。
详细部署,请参考[部署指南](/project/docs/deploy-guide)。
有关 Crm 的详细部署文档,请参考[部署指南](/project/docs/deploy-guide)。
+10 -5
View File
@@ -6,7 +6,7 @@
## 什么是 Crm
Crm(英文全称 Customer relationship management )是一个客户关系管理系统,主要功能有仪表盘、客户管理、合同管理、产品管理、订阅等功能。
Crm(英文全称 Customer relationship management )是一个客户关系管理系统,主要功能有仪表盘、客户管理、合同管理、产品管理、配置、订阅等功能。
- 在线演示:[zocrm.cloud](https://zocrm.cloud)
@@ -14,6 +14,8 @@ Crm(英文全称 Customer relationship management )是一个客户关系管
## 技术栈
Crm 系统主要采用 Vue3 和 Golang 实现。
### 前端技术
| 技术 | 说明 | 相关文档 |
@@ -36,7 +38,8 @@ Crm(英文全称 Customer relationship management )是一个客户关系管
| Jwt | 用户认证 | https://github.com/golang-jwt/jwt |
| Viper | 配置管理 | https://github.com/spf13/viper |
| Redis | 数据缓存 | https://github.com/go-redis/redis |
| Mail | 邮件服务SDK | https://github.com/go-gomail/gomail |
| Gomail | 邮件服务SDK | https://github.com/go-gomail/gomail |
| Gopay | 支付服务SDK | https://github.com/go-pay/gopay |
## 目录结构
@@ -59,6 +62,7 @@ crm
├── api // API层
├── common // 通用的工具
├── config // 配置文件
├── dao // 数据访问层
├── db // 数据库 SQL 文件
├── global // 全局对象
├── initialize // 初始化
@@ -76,11 +80,14 @@ crm
|── api // API接口
├── assets // 资源
├── axios // 网络请求
├── components // 自定义组件
├── router // 页面路由
├── store // 状态管理
├── views // 页面
├── App.vue // 组件入口
├── main.js // 程序启动的入口
├── .env.dev // 开发模式环境变量
├── .env.prod // 生产模式环境变量
├── index.html // 首页
├── package-lock.json // Npm依赖管理
├── package.json // Npm依赖管理
@@ -91,8 +98,6 @@ crm
├── README.md // 项目简介文档
```
想要了解有关 Crm 目录结构中的文件,请访问[github.com/zchengo/crm](https://github.com/zchengo/crm)代码库。
## 系统架构
Crm 系统采用前后端分离架构,前端与后端分开部署,且部署到同一台服务器。
@@ -103,4 +108,4 @@ Crm 系统采用前后端分离架构,前端与后端分开部署,且部署
## 许可证
Crm 是采用 MIT 许可的开源项目,使用完全免费。要了解有关 MIT 许可证的更多信息,请访问[MIT License](https://github.com/zchengo/crm/blob/main/LICENSE)。
Crm 是采用 MIT 许可的开源项目,使用完全免费。要了解有关 MIT 许可证的更多信息,请访问[MIT License](https://github.com/zchengo/crm/blob/main/LICENSE)。
@@ -2,5 +2,4 @@
在使用过程中遇到了问题,您可以通过以下方式反馈:
- [在公众号后台反馈](/about/about#关注作者)
- [New Issues In Github](https://github.com/zchengo/crm/issues)
+11 -32
View File
@@ -1,6 +1,8 @@
# 封装 axios 请求库
# 网络请求库封装
## 什么是 axios
Crm 系统采用 axios 作为网络请求库。
## 什么是 axios
axios 是浏览器和 node.js 的一个简单的基于 promise 的 HTTP 客户端。axios 在一个具有非常可扩展界面的小软件包中提供了一个简单易用的库。
@@ -12,26 +14,16 @@ axios 是浏览器和 node.js 的一个简单的基于 promise 的 HTTP 客户
```js
import axios from 'axios';
import router from '../router/index';
import { message } from 'ant-design-vue';
const host = window.location.hostname
switch (host) {
case 'zocrm.cloud':
axios.defaults.baseURL = 'https://zocrm.cloud/api'
break;
default:
axios.defaults.baseURL = 'http://127.0.0.1:8000/api'
break;
}
axios.defaults.baseURL = import.meta.env.VITE_API_BASE_URL
const request = axios.create({
timeout: 5000,
// timeout: 5000,`
headers: {
'Content-Type': 'application/json;charset=UTF-8'
}
});
})
request.interceptors.request.use(config => {
config.headers['uid'] = localStorage.getItem('uid')
@@ -47,7 +39,6 @@ request.interceptors.response.use(response => {
return response;
}, error => {
console.log(error)
router.push('/error');
return Promise.reject(error)
})
@@ -57,29 +48,20 @@ export default request;
### 初始化请求的 baseURL
```js
const host = window.location.hostname
switch (host) {
case 'zocrm.cloud':
axios.defaults.baseURL = 'https://zocrm.cloud/api'
break;
default:
axios.defaults.baseURL = 'http://127.0.0.1:8000/api'
break;
}
axios.defaults.baseURL = import.meta.env.VITE_API_BASE_URL
```
通过 ```window.location.hostname``` 获取主机名(不包含端口号),主机名指的是 ```IP地址``` 或 ```域名```,然后根据不同的主机名,设置不同的 ```axios.defaults.baseURL```。
通过 ```import.meta.env.VITE_API_BASE_URL``` 获取环境变量
### 设置请求超时时间与请求头
```js
const request = axios.create({
timeout: 5000,
// timeout: 5000,`
headers: {
'Content-Type': 'application/json;charset=UTF-8'
}
});
})
```
### 请求拦截与响应拦截
@@ -107,13 +89,10 @@ request.interceptors.response.use(response => {
return response;
}, error => {
console.log(error)
router.push('/error');
return Promise.reject(error)
})
```
后端响应的数据会先被 axios 拦截,如果响应的结果是正确的,会直接返回响应结果,然后将数据渲染到指定的页面;如果响应的结果是错误的,会路由到错误页面。
了解有关 axios 请求拦截与响应拦截的更多信息,请访问[axios interceptors](https://github.com/axios/axios#interceptors)。
@@ -0,0 +1,95 @@
# 环境变量和模式
Crm 采用 Vite 提供的环境变量与模式,来设置应用的环境变量与运行模式。
了解有关 Vite 环境变量与模式的更多信息,请参考文档[Vite 环境变量与模式](https://vitejs.dev/guide/env-and-mode.html#env-files)。
## 创建 ```.env``` 文件
在 ```crm/web/``` 目录下创建 ```.env``` 文件,Vite 会从你的 ```环境目录``` 中的下列文件加载额外的环境变量:
```txt
.env # 所有情况下都会加载
.env.local # 所有情况下都会加载,但会被 git 忽略
.env.[mode] # 只在指定模式下加载
.env.[mode].local # 只在指定模式下加载,但会被 git 忽略
```
在 Crm 系统中,只定义了开发模式与生产模式:
```txt
.env.dev # 开发模式的环境变量
.env.prod # 生产模式的环境变量
```
## 定义环境变量
分别在开发模式和生产模式中定义开发环境和生产环境所需要环境变量。
请参考 ```crm/web/.env.dev``` 文件如下:
```txt
# .env.development
VITE_API_BASE_URL=http://127.0.0.1:8000/api
VITE_FILE_UPLOAD_URL=http://127.0.0.1:8000/api/common/file/upload
```
请参考 ```crm/web/.env.prod``` 文件如下:
```txt
# .env.production
VITE_API_BASE_URL=https://zocrm.cloud/api
VITE_FILE_UPLOAD_URL=https://zocrm.cloud/api/common/file/upload
```
## 加载环境变量
通过 ```import.meta.env``` 来获得环境变量:
```js
console.log(import.meta.env.VITE_API_BASE_URL) // http://127.0.0.1:8000/api
console.log(import.meta.env.VITE_FILE_UPLOAD_URL) // http://127.0.0.1:8000/api/common/file/upload
```
请注意,Vite 只会加载以 ```VITE_``` 为前缀的变量。
在 ```crm/web/src/axios/index.js``` 中加载环境变量:
```js
import axios from 'axios';
import { message } from 'ant-design-vue';
axios.defaults.baseURL = import.meta.env.VITE_API_BASE_URL
const request = axios.create({
// timeout: 5000,`
headers: {
'Content-Type': 'application/json;charset=UTF-8'
}
})
...
```
## 运行模式
在 ```crm/web/package.json``` 中添加运行模式,请参考如下:
```json
{
"name": "web",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite --mode dev",
"build": "vite build --mode prod",
"preview": "vite preview"
},
"dependencies": {
...
},
"devDependencies": {
...
}
}
```
+1 -1
View File
@@ -8,4 +8,4 @@
| 用户昵称 | 金额 | 备注 | 赞赏日期 |
| ---- | -- | -- | -- |
| 空空如也 | ¥0 | 无备注 | 2023.01.10 |
| 罗**弦 | ¥5 | 学习您的项目 | 2023.02.23 |
+260 -248
View File
@@ -9,24 +9,24 @@
"version": "1.0.0",
"license": "MIT",
"dependencies": {
"vitepress": "^1.0.0-alpha.35",
"vitepress": "^1.0.0-alpha.47",
"vue": "^3.2.45"
}
},
"node_modules/@algolia/autocomplete-core": {
"version": "1.7.2",
"resolved": "https://registry.npmmirror.com/@algolia/autocomplete-core/-/autocomplete-core-1.7.2.tgz",
"integrity": "sha512-eclwUDC6qfApNnEfu1uWcL/rudQsn59tjEoUYZYE2JSXZrHLRjBUGMxiCoknobU2Pva8ejb0eRxpIYDtVVqdsw==",
"version": "1.7.4",
"resolved": "https://registry.npmmirror.com/@algolia/autocomplete-core/-/autocomplete-core-1.7.4.tgz",
"integrity": "sha512-daoLpQ3ps/VTMRZDEBfU8ixXd+amZcNJ4QSP3IERGyzqnL5Ch8uSRFt/4G8pUvW9c3o6GA4vtVv4I4lmnkdXyg==",
"dependencies": {
"@algolia/autocomplete-shared": "1.7.2"
"@algolia/autocomplete-shared": "1.7.4"
}
},
"node_modules/@algolia/autocomplete-preset-algolia": {
"version": "1.7.2",
"resolved": "https://registry.npmmirror.com/@algolia/autocomplete-preset-algolia/-/autocomplete-preset-algolia-1.7.2.tgz",
"integrity": "sha512-+RYEG6B0QiGGfRb2G3MtPfyrl0dALF3cQNTWBzBX6p5o01vCCGTTinAm2UKG3tfc2CnOMAtnPLkzNZyJUpnVJw==",
"version": "1.7.4",
"resolved": "https://registry.npmmirror.com/@algolia/autocomplete-preset-algolia/-/autocomplete-preset-algolia-1.7.4.tgz",
"integrity": "sha512-s37hrvLEIfcmKY8VU9LsAXgm2yfmkdHT3DnA3SgHaY93yjZ2qL57wzb5QweVkYuEBZkT2PIREvRoLXC2sxTbpQ==",
"dependencies": {
"@algolia/autocomplete-shared": "1.7.2"
"@algolia/autocomplete-shared": "1.7.4"
},
"peerDependencies": {
"@algolia/client-search": ">= 4.9.1 < 6",
@@ -34,9 +34,9 @@
}
},
"node_modules/@algolia/autocomplete-shared": {
"version": "1.7.2",
"resolved": "https://registry.npmmirror.com/@algolia/autocomplete-shared/-/autocomplete-shared-1.7.2.tgz",
"integrity": "sha512-QCckjiC7xXHIUaIL3ektBtjJ0w7tTA3iqKcAE/Hjn1lZ5omp7i3Y4e09rAr9ZybqirL7AbxCLLq0Ra5DDPKeug=="
"version": "1.7.4",
"resolved": "https://registry.npmmirror.com/@algolia/autocomplete-shared/-/autocomplete-shared-1.7.4.tgz",
"integrity": "sha512-2VGCk7I9tA9Ge73Km99+Qg87w0wzW4tgUruvWAn/gfey1ZXgmxZtyIRBebk35R1O8TbK77wujVtCnpsGpRy1kg=="
},
"node_modules/@algolia/cache-browser-local-storage": {
"version": "4.14.3",
@@ -154,9 +154,9 @@
}
},
"node_modules/@babel/parser": {
"version": "7.20.7",
"resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.20.7.tgz",
"integrity": "sha512-T3Z9oHybU+0vZlY9CiDSJQTD5ZapcW18ZctFMi0MOAl/4BjFF4ul7NVSARLdbGO5vDqy9eQiGTV0LtKfvCYvcg==",
"version": "7.21.2",
"resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.21.2.tgz",
"integrity": "sha512-URpaIJQwEkEC2T9Kn+Ai6Xe/02iNaVCuT/PtoRz3GPVJVDpPd7mLo+VddTbhCRU9TXqW5mSrQfXZyi8kDKOVpQ==",
"bin": {
"parser": "bin/babel-parser.js"
},
@@ -165,27 +165,27 @@
}
},
"node_modules/@docsearch/css": {
"version": "3.3.1",
"resolved": "https://registry.npmmirror.com/@docsearch/css/-/css-3.3.1.tgz",
"integrity": "sha512-nznHXeFHpAYjyaSNFNFpU+IJPjQA7AINM8ONjDx/Zx4O/pGAvqwgmcLNc7zR8qXRutqnzLo06yN63xFn36KFBw=="
"version": "3.3.3",
"resolved": "https://registry.npmmirror.com/@docsearch/css/-/css-3.3.3.tgz",
"integrity": "sha512-6SCwI7P8ao+se1TUsdZ7B4XzL+gqeQZnBc+2EONZlcVa0dVrk0NjETxozFKgMv0eEGH8QzP1fkN+A1rH61l4eg=="
},
"node_modules/@docsearch/js": {
"version": "3.3.1",
"resolved": "https://registry.npmmirror.com/@docsearch/js/-/js-3.3.1.tgz",
"integrity": "sha512-BCVu7njUFJSUXDNvgK65xNYU1L7U3CKFJlawDXql17nQwfpBrNZHqp+eb8z9qu0SzauQKss9tsf/qwlFJ9BOGw==",
"version": "3.3.3",
"resolved": "https://registry.npmmirror.com/@docsearch/js/-/js-3.3.3.tgz",
"integrity": "sha512-2xAv2GFuHzzmG0SSZgf8wHX0qZX8n9Y1ZirKUk5Wrdc+vH9CL837x2hZIUdwcPZI9caBA+/CzxsS68O4waYjUQ==",
"dependencies": {
"@docsearch/react": "3.3.1",
"@docsearch/react": "3.3.3",
"preact": "^10.0.0"
}
},
"node_modules/@docsearch/react": {
"version": "3.3.1",
"resolved": "https://registry.npmmirror.com/@docsearch/react/-/react-3.3.1.tgz",
"integrity": "sha512-wdeQBODPkue6yVEEg4ntt+TiGJ6iXMBUNjBQJ0s1WVoc1OdcCnks/lkQ5LEfXETYR/q9QSbCCBnMjvnSoILaag==",
"version": "3.3.3",
"resolved": "https://registry.npmmirror.com/@docsearch/react/-/react-3.3.3.tgz",
"integrity": "sha512-pLa0cxnl+G0FuIDuYlW+EBK6Rw2jwLw9B1RHIeS4N4s2VhsfJ/wzeCi3CWcs5yVfxLd5ZK50t//TMA5e79YT7Q==",
"dependencies": {
"@algolia/autocomplete-core": "1.7.2",
"@algolia/autocomplete-preset-algolia": "1.7.2",
"@docsearch/css": "3.3.1",
"@algolia/autocomplete-core": "1.7.4",
"@algolia/autocomplete-preset-algolia": "1.7.4",
"@docsearch/css": "3.3.3",
"algoliasearch": "^4.0.0"
},
"peerDependencies": {
@@ -553,36 +553,36 @@
}
},
"node_modules/@vue/compiler-core": {
"version": "3.2.45",
"resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.2.45.tgz",
"integrity": "sha512-rcMj7H+PYe5wBV3iYeUgbCglC+pbpN8hBLTJvRiK2eKQiWqu+fG9F+8sW99JdL4LQi7Re178UOxn09puSXvn4A==",
"version": "3.2.47",
"resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.2.47.tgz",
"integrity": "sha512-p4D7FDnQb7+YJmO2iPEv0SQNeNzcbHdGByJDsT4lynf63AFkOTFN07HsiRSvjGo0QrxR/o3d0hUyNCUnBU2Tig==",
"dependencies": {
"@babel/parser": "^7.16.4",
"@vue/shared": "3.2.45",
"@vue/shared": "3.2.47",
"estree-walker": "^2.0.2",
"source-map": "^0.6.1"
}
},
"node_modules/@vue/compiler-dom": {
"version": "3.2.45",
"resolved": "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.2.45.tgz",
"integrity": "sha512-tyYeUEuKqqZO137WrZkpwfPCdiiIeXYCcJ8L4gWz9vqaxzIQRccTSwSWZ/Axx5YR2z+LvpUbmPNXxuBU45lyRw==",
"version": "3.2.47",
"resolved": "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.2.47.tgz",
"integrity": "sha512-dBBnEHEPoftUiS03a4ggEig74J2YBZ2UIeyfpcRM2tavgMWo4bsEfgCGsu+uJIL/vax9S+JztH8NmQerUo7shQ==",
"dependencies": {
"@vue/compiler-core": "3.2.45",
"@vue/shared": "3.2.45"
"@vue/compiler-core": "3.2.47",
"@vue/shared": "3.2.47"
}
},
"node_modules/@vue/compiler-sfc": {
"version": "3.2.45",
"resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.2.45.tgz",
"integrity": "sha512-1jXDuWah1ggsnSAOGsec8cFjT/K6TMZ0sPL3o3d84Ft2AYZi2jWJgRMjw4iaK0rBfA89L5gw427H4n1RZQBu6Q==",
"version": "3.2.47",
"resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.2.47.tgz",
"integrity": "sha512-rog05W+2IFfxjMcFw10tM9+f7i/+FFpZJJ5XHX72NP9eC2uRD+42M3pYcQqDXVYoj74kHMSEdQ/WmCjt8JFksQ==",
"dependencies": {
"@babel/parser": "^7.16.4",
"@vue/compiler-core": "3.2.45",
"@vue/compiler-dom": "3.2.45",
"@vue/compiler-ssr": "3.2.45",
"@vue/reactivity-transform": "3.2.45",
"@vue/shared": "3.2.45",
"@vue/compiler-core": "3.2.47",
"@vue/compiler-dom": "3.2.47",
"@vue/compiler-ssr": "3.2.47",
"@vue/reactivity-transform": "3.2.47",
"@vue/shared": "3.2.47",
"estree-walker": "^2.0.2",
"magic-string": "^0.25.7",
"postcss": "^8.1.10",
@@ -590,83 +590,83 @@
}
},
"node_modules/@vue/compiler-ssr": {
"version": "3.2.45",
"resolved": "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.2.45.tgz",
"integrity": "sha512-6BRaggEGqhWht3lt24CrIbQSRD5O07MTmd+LjAn5fJj568+R9eUD2F7wMQJjX859seSlrYog7sUtrZSd7feqrQ==",
"version": "3.2.47",
"resolved": "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.2.47.tgz",
"integrity": "sha512-wVXC+gszhulcMD8wpxMsqSOpvDZ6xKXSVWkf50Guf/S+28hTAXPDYRTbLQ3EDkOP5Xz/+SY37YiwDquKbJOgZw==",
"dependencies": {
"@vue/compiler-dom": "3.2.45",
"@vue/shared": "3.2.45"
"@vue/compiler-dom": "3.2.47",
"@vue/shared": "3.2.47"
}
},
"node_modules/@vue/devtools-api": {
"version": "6.4.5",
"resolved": "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-6.4.5.tgz",
"integrity": "sha512-JD5fcdIuFxU4fQyXUu3w2KpAJHzTVdN+p4iOX2lMWSHMOoQdMAcpFLZzm9Z/2nmsoZ1a96QEhZ26e50xLBsgOQ=="
"version": "6.5.0",
"resolved": "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-6.5.0.tgz",
"integrity": "sha512-o9KfBeaBmCKl10usN4crU53fYtC1r7jJwdGKjPT24t348rHxgfpZ0xL3Xm/gLUYnc0oTp8LAmrxOeLyu6tbk2Q=="
},
"node_modules/@vue/reactivity": {
"version": "3.2.45",
"resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.2.45.tgz",
"integrity": "sha512-PRvhCcQcyEVohW0P8iQ7HDcIOXRjZfAsOds3N99X/Dzewy8TVhTCT4uXpAHfoKjVTJRA0O0K+6QNkDIZAxNi3A==",
"version": "3.2.47",
"resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.2.47.tgz",
"integrity": "sha512-7khqQ/75oyyg+N/e+iwV6lpy1f5wq759NdlS1fpAhFXa8VeAIKGgk2E/C4VF59lx5b+Ezs5fpp/5WsRYXQiKxQ==",
"dependencies": {
"@vue/shared": "3.2.45"
"@vue/shared": "3.2.47"
}
},
"node_modules/@vue/reactivity-transform": {
"version": "3.2.45",
"resolved": "https://registry.npmmirror.com/@vue/reactivity-transform/-/reactivity-transform-3.2.45.tgz",
"integrity": "sha512-BHVmzYAvM7vcU5WmuYqXpwaBHjsS8T63jlKGWVtHxAHIoMIlmaMyurUSEs1Zcg46M4AYT5MtB1U274/2aNzjJQ==",
"version": "3.2.47",
"resolved": "https://registry.npmmirror.com/@vue/reactivity-transform/-/reactivity-transform-3.2.47.tgz",
"integrity": "sha512-m8lGXw8rdnPVVIdIFhf0LeQ/ixyHkH5plYuS83yop5n7ggVJU+z5v0zecwEnX7fa7HNLBhh2qngJJkxpwEEmYA==",
"dependencies": {
"@babel/parser": "^7.16.4",
"@vue/compiler-core": "3.2.45",
"@vue/shared": "3.2.45",
"@vue/compiler-core": "3.2.47",
"@vue/shared": "3.2.47",
"estree-walker": "^2.0.2",
"magic-string": "^0.25.7"
}
},
"node_modules/@vue/runtime-core": {
"version": "3.2.45",
"resolved": "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.2.45.tgz",
"integrity": "sha512-gzJiTA3f74cgARptqzYswmoQx0fIA+gGYBfokYVhF8YSXjWTUA2SngRzZRku2HbGbjzB6LBYSbKGIaK8IW+s0A==",
"version": "3.2.47",
"resolved": "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.2.47.tgz",
"integrity": "sha512-RZxbLQIRB/K0ev0K9FXhNbBzT32H9iRtYbaXb0ZIz2usLms/D55dJR2t6cIEUn6vyhS3ALNvNthI+Q95C+NOpA==",
"dependencies": {
"@vue/reactivity": "3.2.45",
"@vue/shared": "3.2.45"
"@vue/reactivity": "3.2.47",
"@vue/shared": "3.2.47"
}
},
"node_modules/@vue/runtime-dom": {
"version": "3.2.45",
"resolved": "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.2.45.tgz",
"integrity": "sha512-cy88YpfP5Ue2bDBbj75Cb4bIEZUMM/mAkDMfqDTpUYVgTf/kuQ2VQ8LebuZ8k6EudgH8pYhsGWHlY0lcxlvTwA==",
"version": "3.2.47",
"resolved": "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.2.47.tgz",
"integrity": "sha512-ArXrFTjS6TsDei4qwNvgrdmHtD930KgSKGhS5M+j8QxXrDJYLqYw4RRcDy1bz1m1wMmb6j+zGLifdVHtkXA7gA==",
"dependencies": {
"@vue/runtime-core": "3.2.45",
"@vue/shared": "3.2.45",
"@vue/runtime-core": "3.2.47",
"@vue/shared": "3.2.47",
"csstype": "^2.6.8"
}
},
"node_modules/@vue/server-renderer": {
"version": "3.2.45",
"resolved": "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.2.45.tgz",
"integrity": "sha512-ebiMq7q24WBU1D6uhPK//2OTR1iRIyxjF5iVq/1a5I1SDMDyDu4Ts6fJaMnjrvD3MqnaiFkKQj+LKAgz5WIK3g==",
"version": "3.2.47",
"resolved": "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.2.47.tgz",
"integrity": "sha512-dN9gc1i8EvmP9RCzvneONXsKfBRgqFeFZLurmHOveL7oH6HiFXJw5OGu294n1nHc/HMgTy6LulU/tv5/A7f/LA==",
"dependencies": {
"@vue/compiler-ssr": "3.2.45",
"@vue/shared": "3.2.45"
"@vue/compiler-ssr": "3.2.47",
"@vue/shared": "3.2.47"
},
"peerDependencies": {
"vue": "3.2.45"
"vue": "3.2.47"
}
},
"node_modules/@vue/shared": {
"version": "3.2.45",
"resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.2.45.tgz",
"integrity": "sha512-Ewzq5Yhimg7pSztDV+RH1UDKBzmtqieXQlpTVm2AwraoRL/Rks96mvd8Vgi7Lj+h+TH8dv7mXD3FRZR3TUvbSg=="
"version": "3.2.47",
"resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.2.47.tgz",
"integrity": "sha512-BHGyyGN3Q97EZx0taMQ+OLNuZcW3d37ZEVmEAyeoA9ERdGvm9Irc/0Fua8SNyOtV1w6BS4q25wbMzJujO9HIfQ=="
},
"node_modules/@vueuse/core": {
"version": "9.10.0",
"resolved": "https://registry.npmmirror.com/@vueuse/core/-/core-9.10.0.tgz",
"integrity": "sha512-CxMewME07qeuzuT/AOIQGv0EhhDoojniqU6pC3F8m5VC76L47UT18DcX88kWlP3I7d3qMJ4u/PD8iSRsy3bmNA==",
"version": "9.13.0",
"resolved": "https://registry.npmmirror.com/@vueuse/core/-/core-9.13.0.tgz",
"integrity": "sha512-pujnclbeHWxxPRqXWmdkKV5OX4Wk4YeK7wusHqRwU0Q7EFusHoqNA/aPhB6KCh9hEqJkLAJo7bb0Lh9b+OIVzw==",
"dependencies": {
"@types/web-bluetooth": "^0.0.16",
"@vueuse/metadata": "9.10.0",
"@vueuse/shared": "9.10.0",
"@vueuse/metadata": "9.13.0",
"@vueuse/shared": "9.13.0",
"vue-demi": "*"
}
},
@@ -693,14 +693,14 @@
}
},
"node_modules/@vueuse/metadata": {
"version": "9.10.0",
"resolved": "https://registry.npmmirror.com/@vueuse/metadata/-/metadata-9.10.0.tgz",
"integrity": "sha512-G5VZhgTCapzU9rv0Iq2HBrVOSGzOKb+OE668NxhXNcTjUjwYxULkEhAw70FtRLMZc+hxcFAzDZlKYA0xcwNMuw=="
"version": "9.13.0",
"resolved": "https://registry.npmmirror.com/@vueuse/metadata/-/metadata-9.13.0.tgz",
"integrity": "sha512-gdU7TKNAUVlXXLbaF+ZCfte8BjRJQWPCa2J55+7/h+yDtzw3vOoGQDRXzI6pyKyo6bXFT5/QoPE4hAknExjRLQ=="
},
"node_modules/@vueuse/shared": {
"version": "9.10.0",
"resolved": "https://registry.npmmirror.com/@vueuse/shared/-/shared-9.10.0.tgz",
"integrity": "sha512-vakHJ2ZRklAzqmcVBL38RS7BxdBA4+5poG9NsSyqJxrt9kz0zX3P5CXMy0Hm6LFbZXUgvKdqAS3pUH1zX/5qTQ==",
"version": "9.13.0",
"resolved": "https://registry.npmmirror.com/@vueuse/shared/-/shared-9.13.0.tgz",
"integrity": "sha512-UrnhU+Cnufu4S6JLCPZnkWh0WwZGUp72ktOF2DFptMlOs3TOdVv8xJN53zhHGARmVOsz5KqOls09+J1NR6sBKw==",
"dependencies": {
"vue-demi": "*"
}
@@ -748,6 +748,11 @@
"@algolia/transporter": "4.14.3"
}
},
"node_modules/ansi-sequence-parser": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/ansi-sequence-parser/-/ansi-sequence-parser-1.1.0.tgz",
"integrity": "sha512-lEm8mt52to2fT8GhciPCGeCXACSz2UwIN4X2e2LJSnZ5uAbn2/dsYdOmUXq0AtWS5cpAupysIneExOgH0Vd2TQ=="
},
"node_modules/body-scroll-lock": {
"version": "4.0.0-beta.0",
"resolved": "https://registry.npmmirror.com/body-scroll-lock/-/body-scroll-lock-4.0.0-beta.0.tgz",
@@ -884,9 +889,9 @@
}
},
"node_modules/preact": {
"version": "10.11.3",
"resolved": "https://registry.npmmirror.com/preact/-/preact-10.11.3.tgz",
"integrity": "sha512-eY93IVpod/zG3uMF22Unl8h9KkrcKIRs2EGar8hwLZZDU1lkjph303V9HZBwufh2s736U6VXuhD109LYqPoffg=="
"version": "10.12.1",
"resolved": "https://registry.npmmirror.com/preact/-/preact-10.12.1.tgz",
"integrity": "sha512-l8386ixSsBdbreOAkqtrwqHwdvR35ID8c3rKPa8lCWuO86dBi32QWHV4vfsZK1utLLFMvw+Z5Ad4XLkZzchscg=="
},
"node_modules/resolve": {
"version": "1.22.1",
@@ -902,9 +907,9 @@
}
},
"node_modules/rollup": {
"version": "3.9.1",
"resolved": "https://registry.npmmirror.com/rollup/-/rollup-3.9.1.tgz",
"integrity": "sha512-GswCYHXftN8ZKGVgQhTFUJB/NBXxrRGgO2NCy6E8s1rwEJ4Q9/VttNqcYfEvx4dTo4j58YqdC3OVztPzlKSX8w==",
"version": "3.17.2",
"resolved": "https://registry.npmmirror.com/rollup/-/rollup-3.17.2.tgz",
"integrity": "sha512-qMNZdlQPCkWodrAZ3qnJtvCAl4vpQ8q77uEujVCCbC/6CLB7Lcmvjq7HyiOSnf4fxTT9XgsE36oLHJBH49xjqA==",
"bin": {
"rollup": "dist/bin/rollup"
},
@@ -917,10 +922,11 @@
}
},
"node_modules/shiki": {
"version": "0.12.1",
"resolved": "https://registry.npmmirror.com/shiki/-/shiki-0.12.1.tgz",
"integrity": "sha512-aieaV1m349rZINEBkjxh2QbBvFFQOlgqYTNtCal82hHj4dDZ76oMlQIX+C7ryerBTDiga3e5NfH6smjdJ02BbQ==",
"version": "0.14.1",
"resolved": "https://registry.npmmirror.com/shiki/-/shiki-0.14.1.tgz",
"integrity": "sha512-+Jz4nBkCBe0mEDqo1eKRcCdjRtrCjozmcbTUjbPTX7OOJfEbTZzlUWlZtGe3Gb5oV1/jnojhG//YZc3rs9zSEw==",
"dependencies": {
"ansi-sequence-parser": "^1.1.0",
"jsonc-parser": "^3.2.0",
"vscode-oniguruma": "^1.7.0",
"vscode-textmate": "^8.0.0"
@@ -957,14 +963,14 @@
}
},
"node_modules/vite": {
"version": "4.0.4",
"resolved": "https://registry.npmmirror.com/vite/-/vite-4.0.4.tgz",
"integrity": "sha512-xevPU7M8FU0i/80DMR+YhgrzR5KS2ORy1B4xcX/cXLsvnUWvfHuqMmVU6N0YiJ4JWGRJJsLCgjEzKjG9/GKoSw==",
"version": "4.1.4",
"resolved": "https://registry.npmmirror.com/vite/-/vite-4.1.4.tgz",
"integrity": "sha512-3knk/HsbSTKEin43zHu7jTwYWv81f8kgAL99G5NWBcA1LKvtvcVAC4JjBH1arBunO9kQka+1oGbrMKOjk4ZrBg==",
"dependencies": {
"esbuild": "^0.16.3",
"postcss": "^8.4.20",
"esbuild": "^0.16.14",
"postcss": "^8.4.21",
"resolve": "^1.22.1",
"rollup": "^3.7.0"
"rollup": "^3.10.0"
},
"bin": {
"vite": "bin/vite.js"
@@ -1005,19 +1011,19 @@
}
},
"node_modules/vitepress": {
"version": "1.0.0-alpha.35",
"resolved": "https://registry.npmmirror.com/vitepress/-/vitepress-1.0.0-alpha.35.tgz",
"integrity": "sha512-tJQjJstq+Ryb4pKtlxV4tD8KhxId+DTbR1FRrtJBhA+Vv4nexFHra5M9EgK9jUmoMc3rkyNaw7P3Kkix0ArP1w==",
"version": "1.0.0-alpha.47",
"resolved": "https://registry.npmmirror.com/vitepress/-/vitepress-1.0.0-alpha.47.tgz",
"integrity": "sha512-vj+LOY0WJtKSk98HV4qqG6p4MofmF+C8yrWHiiw+GCMfr6C+610U5D7oD2OruroIafsON6F4nBDWGK8ZyGIpXQ==",
"dependencies": {
"@docsearch/css": "^3.3.1",
"@docsearch/js": "^3.3.1",
"@docsearch/css": "^3.3.3",
"@docsearch/js": "^3.3.3",
"@vitejs/plugin-vue": "^4.0.0",
"@vue/devtools-api": "^6.4.5",
"@vueuse/core": "^9.9.0",
"@vue/devtools-api": "^6.5.0",
"@vueuse/core": "^9.13.0",
"body-scroll-lock": "4.0.0-beta.0",
"shiki": "^0.12.1",
"vite": "^4.0.4",
"vue": "^3.2.45"
"shiki": "^0.14.1",
"vite": "^4.1.3",
"vue": "^3.2.47"
},
"bin": {
"vitepress": "bin/vitepress.js"
@@ -1034,39 +1040,39 @@
"integrity": "sha512-AFbieoL7a5LMqcnOF04ji+rpXadgOXnZsxQr//r83kLPr7biP7am3g9zbaZIaBGwBRWeSvoMD4mgPdX3e4NWBg=="
},
"node_modules/vue": {
"version": "3.2.45",
"resolved": "https://registry.npmmirror.com/vue/-/vue-3.2.45.tgz",
"integrity": "sha512-9Nx/Mg2b2xWlXykmCwiTUCWHbWIj53bnkizBxKai1g61f2Xit700A1ljowpTIM11e3uipOeiPcSqnmBg6gyiaA==",
"version": "3.2.47",
"resolved": "https://registry.npmmirror.com/vue/-/vue-3.2.47.tgz",
"integrity": "sha512-60188y/9Dc9WVrAZeUVSDxRQOZ+z+y5nO2ts9jWXSTkMvayiWxCWOWtBQoYjLeccfXkiiPZWAHcV+WTPhkqJHQ==",
"dependencies": {
"@vue/compiler-dom": "3.2.45",
"@vue/compiler-sfc": "3.2.45",
"@vue/runtime-dom": "3.2.45",
"@vue/server-renderer": "3.2.45",
"@vue/shared": "3.2.45"
"@vue/compiler-dom": "3.2.47",
"@vue/compiler-sfc": "3.2.47",
"@vue/runtime-dom": "3.2.47",
"@vue/server-renderer": "3.2.47",
"@vue/shared": "3.2.47"
}
}
},
"dependencies": {
"@algolia/autocomplete-core": {
"version": "1.7.2",
"resolved": "https://registry.npmmirror.com/@algolia/autocomplete-core/-/autocomplete-core-1.7.2.tgz",
"integrity": "sha512-eclwUDC6qfApNnEfu1uWcL/rudQsn59tjEoUYZYE2JSXZrHLRjBUGMxiCoknobU2Pva8ejb0eRxpIYDtVVqdsw==",
"version": "1.7.4",
"resolved": "https://registry.npmmirror.com/@algolia/autocomplete-core/-/autocomplete-core-1.7.4.tgz",
"integrity": "sha512-daoLpQ3ps/VTMRZDEBfU8ixXd+amZcNJ4QSP3IERGyzqnL5Ch8uSRFt/4G8pUvW9c3o6GA4vtVv4I4lmnkdXyg==",
"requires": {
"@algolia/autocomplete-shared": "1.7.2"
"@algolia/autocomplete-shared": "1.7.4"
}
},
"@algolia/autocomplete-preset-algolia": {
"version": "1.7.2",
"resolved": "https://registry.npmmirror.com/@algolia/autocomplete-preset-algolia/-/autocomplete-preset-algolia-1.7.2.tgz",
"integrity": "sha512-+RYEG6B0QiGGfRb2G3MtPfyrl0dALF3cQNTWBzBX6p5o01vCCGTTinAm2UKG3tfc2CnOMAtnPLkzNZyJUpnVJw==",
"version": "1.7.4",
"resolved": "https://registry.npmmirror.com/@algolia/autocomplete-preset-algolia/-/autocomplete-preset-algolia-1.7.4.tgz",
"integrity": "sha512-s37hrvLEIfcmKY8VU9LsAXgm2yfmkdHT3DnA3SgHaY93yjZ2qL57wzb5QweVkYuEBZkT2PIREvRoLXC2sxTbpQ==",
"requires": {
"@algolia/autocomplete-shared": "1.7.2"
"@algolia/autocomplete-shared": "1.7.4"
}
},
"@algolia/autocomplete-shared": {
"version": "1.7.2",
"resolved": "https://registry.npmmirror.com/@algolia/autocomplete-shared/-/autocomplete-shared-1.7.2.tgz",
"integrity": "sha512-QCckjiC7xXHIUaIL3ektBtjJ0w7tTA3iqKcAE/Hjn1lZ5omp7i3Y4e09rAr9ZybqirL7AbxCLLq0Ra5DDPKeug=="
"version": "1.7.4",
"resolved": "https://registry.npmmirror.com/@algolia/autocomplete-shared/-/autocomplete-shared-1.7.4.tgz",
"integrity": "sha512-2VGCk7I9tA9Ge73Km99+Qg87w0wzW4tgUruvWAn/gfey1ZXgmxZtyIRBebk35R1O8TbK77wujVtCnpsGpRy1kg=="
},
"@algolia/cache-browser-local-storage": {
"version": "4.14.3",
@@ -1184,32 +1190,32 @@
}
},
"@babel/parser": {
"version": "7.20.7",
"resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.20.7.tgz",
"integrity": "sha512-T3Z9oHybU+0vZlY9CiDSJQTD5ZapcW18ZctFMi0MOAl/4BjFF4ul7NVSARLdbGO5vDqy9eQiGTV0LtKfvCYvcg=="
"version": "7.21.2",
"resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.21.2.tgz",
"integrity": "sha512-URpaIJQwEkEC2T9Kn+Ai6Xe/02iNaVCuT/PtoRz3GPVJVDpPd7mLo+VddTbhCRU9TXqW5mSrQfXZyi8kDKOVpQ=="
},
"@docsearch/css": {
"version": "3.3.1",
"resolved": "https://registry.npmmirror.com/@docsearch/css/-/css-3.3.1.tgz",
"integrity": "sha512-nznHXeFHpAYjyaSNFNFpU+IJPjQA7AINM8ONjDx/Zx4O/pGAvqwgmcLNc7zR8qXRutqnzLo06yN63xFn36KFBw=="
"version": "3.3.3",
"resolved": "https://registry.npmmirror.com/@docsearch/css/-/css-3.3.3.tgz",
"integrity": "sha512-6SCwI7P8ao+se1TUsdZ7B4XzL+gqeQZnBc+2EONZlcVa0dVrk0NjETxozFKgMv0eEGH8QzP1fkN+A1rH61l4eg=="
},
"@docsearch/js": {
"version": "3.3.1",
"resolved": "https://registry.npmmirror.com/@docsearch/js/-/js-3.3.1.tgz",
"integrity": "sha512-BCVu7njUFJSUXDNvgK65xNYU1L7U3CKFJlawDXql17nQwfpBrNZHqp+eb8z9qu0SzauQKss9tsf/qwlFJ9BOGw==",
"version": "3.3.3",
"resolved": "https://registry.npmmirror.com/@docsearch/js/-/js-3.3.3.tgz",
"integrity": "sha512-2xAv2GFuHzzmG0SSZgf8wHX0qZX8n9Y1ZirKUk5Wrdc+vH9CL837x2hZIUdwcPZI9caBA+/CzxsS68O4waYjUQ==",
"requires": {
"@docsearch/react": "3.3.1",
"@docsearch/react": "3.3.3",
"preact": "^10.0.0"
}
},
"@docsearch/react": {
"version": "3.3.1",
"resolved": "https://registry.npmmirror.com/@docsearch/react/-/react-3.3.1.tgz",
"integrity": "sha512-wdeQBODPkue6yVEEg4ntt+TiGJ6iXMBUNjBQJ0s1WVoc1OdcCnks/lkQ5LEfXETYR/q9QSbCCBnMjvnSoILaag==",
"version": "3.3.3",
"resolved": "https://registry.npmmirror.com/@docsearch/react/-/react-3.3.3.tgz",
"integrity": "sha512-pLa0cxnl+G0FuIDuYlW+EBK6Rw2jwLw9B1RHIeS4N4s2VhsfJ/wzeCi3CWcs5yVfxLd5ZK50t//TMA5e79YT7Q==",
"requires": {
"@algolia/autocomplete-core": "1.7.2",
"@algolia/autocomplete-preset-algolia": "1.7.2",
"@docsearch/css": "3.3.1",
"@algolia/autocomplete-core": "1.7.4",
"@algolia/autocomplete-preset-algolia": "1.7.4",
"@docsearch/css": "3.3.3",
"algoliasearch": "^4.0.0"
}
},
@@ -1357,36 +1363,36 @@
"requires": {}
},
"@vue/compiler-core": {
"version": "3.2.45",
"resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.2.45.tgz",
"integrity": "sha512-rcMj7H+PYe5wBV3iYeUgbCglC+pbpN8hBLTJvRiK2eKQiWqu+fG9F+8sW99JdL4LQi7Re178UOxn09puSXvn4A==",
"version": "3.2.47",
"resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.2.47.tgz",
"integrity": "sha512-p4D7FDnQb7+YJmO2iPEv0SQNeNzcbHdGByJDsT4lynf63AFkOTFN07HsiRSvjGo0QrxR/o3d0hUyNCUnBU2Tig==",
"requires": {
"@babel/parser": "^7.16.4",
"@vue/shared": "3.2.45",
"@vue/shared": "3.2.47",
"estree-walker": "^2.0.2",
"source-map": "^0.6.1"
}
},
"@vue/compiler-dom": {
"version": "3.2.45",
"resolved": "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.2.45.tgz",
"integrity": "sha512-tyYeUEuKqqZO137WrZkpwfPCdiiIeXYCcJ8L4gWz9vqaxzIQRccTSwSWZ/Axx5YR2z+LvpUbmPNXxuBU45lyRw==",
"version": "3.2.47",
"resolved": "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.2.47.tgz",
"integrity": "sha512-dBBnEHEPoftUiS03a4ggEig74J2YBZ2UIeyfpcRM2tavgMWo4bsEfgCGsu+uJIL/vax9S+JztH8NmQerUo7shQ==",
"requires": {
"@vue/compiler-core": "3.2.45",
"@vue/shared": "3.2.45"
"@vue/compiler-core": "3.2.47",
"@vue/shared": "3.2.47"
}
},
"@vue/compiler-sfc": {
"version": "3.2.45",
"resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.2.45.tgz",
"integrity": "sha512-1jXDuWah1ggsnSAOGsec8cFjT/K6TMZ0sPL3o3d84Ft2AYZi2jWJgRMjw4iaK0rBfA89L5gw427H4n1RZQBu6Q==",
"version": "3.2.47",
"resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.2.47.tgz",
"integrity": "sha512-rog05W+2IFfxjMcFw10tM9+f7i/+FFpZJJ5XHX72NP9eC2uRD+42M3pYcQqDXVYoj74kHMSEdQ/WmCjt8JFksQ==",
"requires": {
"@babel/parser": "^7.16.4",
"@vue/compiler-core": "3.2.45",
"@vue/compiler-dom": "3.2.45",
"@vue/compiler-ssr": "3.2.45",
"@vue/reactivity-transform": "3.2.45",
"@vue/shared": "3.2.45",
"@vue/compiler-core": "3.2.47",
"@vue/compiler-dom": "3.2.47",
"@vue/compiler-ssr": "3.2.47",
"@vue/reactivity-transform": "3.2.47",
"@vue/shared": "3.2.47",
"estree-walker": "^2.0.2",
"magic-string": "^0.25.7",
"postcss": "^8.1.10",
@@ -1394,80 +1400,80 @@
}
},
"@vue/compiler-ssr": {
"version": "3.2.45",
"resolved": "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.2.45.tgz",
"integrity": "sha512-6BRaggEGqhWht3lt24CrIbQSRD5O07MTmd+LjAn5fJj568+R9eUD2F7wMQJjX859seSlrYog7sUtrZSd7feqrQ==",
"version": "3.2.47",
"resolved": "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.2.47.tgz",
"integrity": "sha512-wVXC+gszhulcMD8wpxMsqSOpvDZ6xKXSVWkf50Guf/S+28hTAXPDYRTbLQ3EDkOP5Xz/+SY37YiwDquKbJOgZw==",
"requires": {
"@vue/compiler-dom": "3.2.45",
"@vue/shared": "3.2.45"
"@vue/compiler-dom": "3.2.47",
"@vue/shared": "3.2.47"
}
},
"@vue/devtools-api": {
"version": "6.4.5",
"resolved": "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-6.4.5.tgz",
"integrity": "sha512-JD5fcdIuFxU4fQyXUu3w2KpAJHzTVdN+p4iOX2lMWSHMOoQdMAcpFLZzm9Z/2nmsoZ1a96QEhZ26e50xLBsgOQ=="
"version": "6.5.0",
"resolved": "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-6.5.0.tgz",
"integrity": "sha512-o9KfBeaBmCKl10usN4crU53fYtC1r7jJwdGKjPT24t348rHxgfpZ0xL3Xm/gLUYnc0oTp8LAmrxOeLyu6tbk2Q=="
},
"@vue/reactivity": {
"version": "3.2.45",
"resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.2.45.tgz",
"integrity": "sha512-PRvhCcQcyEVohW0P8iQ7HDcIOXRjZfAsOds3N99X/Dzewy8TVhTCT4uXpAHfoKjVTJRA0O0K+6QNkDIZAxNi3A==",
"version": "3.2.47",
"resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.2.47.tgz",
"integrity": "sha512-7khqQ/75oyyg+N/e+iwV6lpy1f5wq759NdlS1fpAhFXa8VeAIKGgk2E/C4VF59lx5b+Ezs5fpp/5WsRYXQiKxQ==",
"requires": {
"@vue/shared": "3.2.45"
"@vue/shared": "3.2.47"
}
},
"@vue/reactivity-transform": {
"version": "3.2.45",
"resolved": "https://registry.npmmirror.com/@vue/reactivity-transform/-/reactivity-transform-3.2.45.tgz",
"integrity": "sha512-BHVmzYAvM7vcU5WmuYqXpwaBHjsS8T63jlKGWVtHxAHIoMIlmaMyurUSEs1Zcg46M4AYT5MtB1U274/2aNzjJQ==",
"version": "3.2.47",
"resolved": "https://registry.npmmirror.com/@vue/reactivity-transform/-/reactivity-transform-3.2.47.tgz",
"integrity": "sha512-m8lGXw8rdnPVVIdIFhf0LeQ/ixyHkH5plYuS83yop5n7ggVJU+z5v0zecwEnX7fa7HNLBhh2qngJJkxpwEEmYA==",
"requires": {
"@babel/parser": "^7.16.4",
"@vue/compiler-core": "3.2.45",
"@vue/shared": "3.2.45",
"@vue/compiler-core": "3.2.47",
"@vue/shared": "3.2.47",
"estree-walker": "^2.0.2",
"magic-string": "^0.25.7"
}
},
"@vue/runtime-core": {
"version": "3.2.45",
"resolved": "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.2.45.tgz",
"integrity": "sha512-gzJiTA3f74cgARptqzYswmoQx0fIA+gGYBfokYVhF8YSXjWTUA2SngRzZRku2HbGbjzB6LBYSbKGIaK8IW+s0A==",
"version": "3.2.47",
"resolved": "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.2.47.tgz",
"integrity": "sha512-RZxbLQIRB/K0ev0K9FXhNbBzT32H9iRtYbaXb0ZIz2usLms/D55dJR2t6cIEUn6vyhS3ALNvNthI+Q95C+NOpA==",
"requires": {
"@vue/reactivity": "3.2.45",
"@vue/shared": "3.2.45"
"@vue/reactivity": "3.2.47",
"@vue/shared": "3.2.47"
}
},
"@vue/runtime-dom": {
"version": "3.2.45",
"resolved": "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.2.45.tgz",
"integrity": "sha512-cy88YpfP5Ue2bDBbj75Cb4bIEZUMM/mAkDMfqDTpUYVgTf/kuQ2VQ8LebuZ8k6EudgH8pYhsGWHlY0lcxlvTwA==",
"version": "3.2.47",
"resolved": "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.2.47.tgz",
"integrity": "sha512-ArXrFTjS6TsDei4qwNvgrdmHtD930KgSKGhS5M+j8QxXrDJYLqYw4RRcDy1bz1m1wMmb6j+zGLifdVHtkXA7gA==",
"requires": {
"@vue/runtime-core": "3.2.45",
"@vue/shared": "3.2.45",
"@vue/runtime-core": "3.2.47",
"@vue/shared": "3.2.47",
"csstype": "^2.6.8"
}
},
"@vue/server-renderer": {
"version": "3.2.45",
"resolved": "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.2.45.tgz",
"integrity": "sha512-ebiMq7q24WBU1D6uhPK//2OTR1iRIyxjF5iVq/1a5I1SDMDyDu4Ts6fJaMnjrvD3MqnaiFkKQj+LKAgz5WIK3g==",
"version": "3.2.47",
"resolved": "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.2.47.tgz",
"integrity": "sha512-dN9gc1i8EvmP9RCzvneONXsKfBRgqFeFZLurmHOveL7oH6HiFXJw5OGu294n1nHc/HMgTy6LulU/tv5/A7f/LA==",
"requires": {
"@vue/compiler-ssr": "3.2.45",
"@vue/shared": "3.2.45"
"@vue/compiler-ssr": "3.2.47",
"@vue/shared": "3.2.47"
}
},
"@vue/shared": {
"version": "3.2.45",
"resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.2.45.tgz",
"integrity": "sha512-Ewzq5Yhimg7pSztDV+RH1UDKBzmtqieXQlpTVm2AwraoRL/Rks96mvd8Vgi7Lj+h+TH8dv7mXD3FRZR3TUvbSg=="
"version": "3.2.47",
"resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.2.47.tgz",
"integrity": "sha512-BHGyyGN3Q97EZx0taMQ+OLNuZcW3d37ZEVmEAyeoA9ERdGvm9Irc/0Fua8SNyOtV1w6BS4q25wbMzJujO9HIfQ=="
},
"@vueuse/core": {
"version": "9.10.0",
"resolved": "https://registry.npmmirror.com/@vueuse/core/-/core-9.10.0.tgz",
"integrity": "sha512-CxMewME07qeuzuT/AOIQGv0EhhDoojniqU6pC3F8m5VC76L47UT18DcX88kWlP3I7d3qMJ4u/PD8iSRsy3bmNA==",
"version": "9.13.0",
"resolved": "https://registry.npmmirror.com/@vueuse/core/-/core-9.13.0.tgz",
"integrity": "sha512-pujnclbeHWxxPRqXWmdkKV5OX4Wk4YeK7wusHqRwU0Q7EFusHoqNA/aPhB6KCh9hEqJkLAJo7bb0Lh9b+OIVzw==",
"requires": {
"@types/web-bluetooth": "^0.0.16",
"@vueuse/metadata": "9.10.0",
"@vueuse/shared": "9.10.0",
"@vueuse/metadata": "9.13.0",
"@vueuse/shared": "9.13.0",
"vue-demi": "*"
},
"dependencies": {
@@ -1480,14 +1486,14 @@
}
},
"@vueuse/metadata": {
"version": "9.10.0",
"resolved": "https://registry.npmmirror.com/@vueuse/metadata/-/metadata-9.10.0.tgz",
"integrity": "sha512-G5VZhgTCapzU9rv0Iq2HBrVOSGzOKb+OE668NxhXNcTjUjwYxULkEhAw70FtRLMZc+hxcFAzDZlKYA0xcwNMuw=="
"version": "9.13.0",
"resolved": "https://registry.npmmirror.com/@vueuse/metadata/-/metadata-9.13.0.tgz",
"integrity": "sha512-gdU7TKNAUVlXXLbaF+ZCfte8BjRJQWPCa2J55+7/h+yDtzw3vOoGQDRXzI6pyKyo6bXFT5/QoPE4hAknExjRLQ=="
},
"@vueuse/shared": {
"version": "9.10.0",
"resolved": "https://registry.npmmirror.com/@vueuse/shared/-/shared-9.10.0.tgz",
"integrity": "sha512-vakHJ2ZRklAzqmcVBL38RS7BxdBA4+5poG9NsSyqJxrt9kz0zX3P5CXMy0Hm6LFbZXUgvKdqAS3pUH1zX/5qTQ==",
"version": "9.13.0",
"resolved": "https://registry.npmmirror.com/@vueuse/shared/-/shared-9.13.0.tgz",
"integrity": "sha512-UrnhU+Cnufu4S6JLCPZnkWh0WwZGUp72ktOF2DFptMlOs3TOdVv8xJN53zhHGARmVOsz5KqOls09+J1NR6sBKw==",
"requires": {
"vue-demi": "*"
},
@@ -1521,6 +1527,11 @@
"@algolia/transporter": "4.14.3"
}
},
"ansi-sequence-parser": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/ansi-sequence-parser/-/ansi-sequence-parser-1.1.0.tgz",
"integrity": "sha512-lEm8mt52to2fT8GhciPCGeCXACSz2UwIN4X2e2LJSnZ5uAbn2/dsYdOmUXq0AtWS5cpAupysIneExOgH0Vd2TQ=="
},
"body-scroll-lock": {
"version": "4.0.0-beta.0",
"resolved": "https://registry.npmmirror.com/body-scroll-lock/-/body-scroll-lock-4.0.0-beta.0.tgz",
@@ -1631,9 +1642,9 @@
}
},
"preact": {
"version": "10.11.3",
"resolved": "https://registry.npmmirror.com/preact/-/preact-10.11.3.tgz",
"integrity": "sha512-eY93IVpod/zG3uMF22Unl8h9KkrcKIRs2EGar8hwLZZDU1lkjph303V9HZBwufh2s736U6VXuhD109LYqPoffg=="
"version": "10.12.1",
"resolved": "https://registry.npmmirror.com/preact/-/preact-10.12.1.tgz",
"integrity": "sha512-l8386ixSsBdbreOAkqtrwqHwdvR35ID8c3rKPa8lCWuO86dBi32QWHV4vfsZK1utLLFMvw+Z5Ad4XLkZzchscg=="
},
"resolve": {
"version": "1.22.1",
@@ -1646,18 +1657,19 @@
}
},
"rollup": {
"version": "3.9.1",
"resolved": "https://registry.npmmirror.com/rollup/-/rollup-3.9.1.tgz",
"integrity": "sha512-GswCYHXftN8ZKGVgQhTFUJB/NBXxrRGgO2NCy6E8s1rwEJ4Q9/VttNqcYfEvx4dTo4j58YqdC3OVztPzlKSX8w==",
"version": "3.17.2",
"resolved": "https://registry.npmmirror.com/rollup/-/rollup-3.17.2.tgz",
"integrity": "sha512-qMNZdlQPCkWodrAZ3qnJtvCAl4vpQ8q77uEujVCCbC/6CLB7Lcmvjq7HyiOSnf4fxTT9XgsE36oLHJBH49xjqA==",
"requires": {
"fsevents": "~2.3.2"
}
},
"shiki": {
"version": "0.12.1",
"resolved": "https://registry.npmmirror.com/shiki/-/shiki-0.12.1.tgz",
"integrity": "sha512-aieaV1m349rZINEBkjxh2QbBvFFQOlgqYTNtCal82hHj4dDZ76oMlQIX+C7ryerBTDiga3e5NfH6smjdJ02BbQ==",
"version": "0.14.1",
"resolved": "https://registry.npmmirror.com/shiki/-/shiki-0.14.1.tgz",
"integrity": "sha512-+Jz4nBkCBe0mEDqo1eKRcCdjRtrCjozmcbTUjbPTX7OOJfEbTZzlUWlZtGe3Gb5oV1/jnojhG//YZc3rs9zSEw==",
"requires": {
"ansi-sequence-parser": "^1.1.0",
"jsonc-parser": "^3.2.0",
"vscode-oniguruma": "^1.7.0",
"vscode-textmate": "^8.0.0"
@@ -1684,31 +1696,31 @@
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="
},
"vite": {
"version": "4.0.4",
"resolved": "https://registry.npmmirror.com/vite/-/vite-4.0.4.tgz",
"integrity": "sha512-xevPU7M8FU0i/80DMR+YhgrzR5KS2ORy1B4xcX/cXLsvnUWvfHuqMmVU6N0YiJ4JWGRJJsLCgjEzKjG9/GKoSw==",
"version": "4.1.4",
"resolved": "https://registry.npmmirror.com/vite/-/vite-4.1.4.tgz",
"integrity": "sha512-3knk/HsbSTKEin43zHu7jTwYWv81f8kgAL99G5NWBcA1LKvtvcVAC4JjBH1arBunO9kQka+1oGbrMKOjk4ZrBg==",
"requires": {
"esbuild": "^0.16.3",
"esbuild": "^0.16.14",
"fsevents": "~2.3.2",
"postcss": "^8.4.20",
"postcss": "^8.4.21",
"resolve": "^1.22.1",
"rollup": "^3.7.0"
"rollup": "^3.10.0"
}
},
"vitepress": {
"version": "1.0.0-alpha.35",
"resolved": "https://registry.npmmirror.com/vitepress/-/vitepress-1.0.0-alpha.35.tgz",
"integrity": "sha512-tJQjJstq+Ryb4pKtlxV4tD8KhxId+DTbR1FRrtJBhA+Vv4nexFHra5M9EgK9jUmoMc3rkyNaw7P3Kkix0ArP1w==",
"version": "1.0.0-alpha.47",
"resolved": "https://registry.npmmirror.com/vitepress/-/vitepress-1.0.0-alpha.47.tgz",
"integrity": "sha512-vj+LOY0WJtKSk98HV4qqG6p4MofmF+C8yrWHiiw+GCMfr6C+610U5D7oD2OruroIafsON6F4nBDWGK8ZyGIpXQ==",
"requires": {
"@docsearch/css": "^3.3.1",
"@docsearch/js": "^3.3.1",
"@docsearch/css": "^3.3.3",
"@docsearch/js": "^3.3.3",
"@vitejs/plugin-vue": "^4.0.0",
"@vue/devtools-api": "^6.4.5",
"@vueuse/core": "^9.9.0",
"@vue/devtools-api": "^6.5.0",
"@vueuse/core": "^9.13.0",
"body-scroll-lock": "4.0.0-beta.0",
"shiki": "^0.12.1",
"vite": "^4.0.4",
"vue": "^3.2.45"
"shiki": "^0.14.1",
"vite": "^4.1.3",
"vue": "^3.2.47"
}
},
"vscode-oniguruma": {
@@ -1722,15 +1734,15 @@
"integrity": "sha512-AFbieoL7a5LMqcnOF04ji+rpXadgOXnZsxQr//r83kLPr7biP7am3g9zbaZIaBGwBRWeSvoMD4mgPdX3e4NWBg=="
},
"vue": {
"version": "3.2.45",
"resolved": "https://registry.npmmirror.com/vue/-/vue-3.2.45.tgz",
"integrity": "sha512-9Nx/Mg2b2xWlXykmCwiTUCWHbWIj53bnkizBxKai1g61f2Xit700A1ljowpTIM11e3uipOeiPcSqnmBg6gyiaA==",
"version": "3.2.47",
"resolved": "https://registry.npmmirror.com/vue/-/vue-3.2.47.tgz",
"integrity": "sha512-60188y/9Dc9WVrAZeUVSDxRQOZ+z+y5nO2ts9jWXSTkMvayiWxCWOWtBQoYjLeccfXkiiPZWAHcV+WTPhkqJHQ==",
"requires": {
"@vue/compiler-dom": "3.2.45",
"@vue/compiler-sfc": "3.2.45",
"@vue/runtime-dom": "3.2.45",
"@vue/server-renderer": "3.2.45",
"@vue/shared": "3.2.45"
"@vue/compiler-dom": "3.2.47",
"@vue/compiler-sfc": "3.2.47",
"@vue/runtime-dom": "3.2.47",
"@vue/server-renderer": "3.2.47",
"@vue/shared": "3.2.47"
}
}
}
+1 -1
View File
@@ -11,7 +11,7 @@
"author": "zchengo",
"license": "MIT",
"dependencies": {
"vitepress": "^1.0.0-alpha.35",
"vitepress": "^1.0.0-alpha.47",
"vue": "^3.2.45"
}
}
+27
View File
@@ -0,0 +1,27 @@
FROM golang:1.21-alpine AS builder
WORKDIR /app
# Set Go Proxy if needed
# RUN go env -w GOPROXY=https://goproxy.cn,direct
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN go build -o main .
FROM alpine:latest
WORKDIR /app
COPY --from=builder /app/main .
COPY --from=builder /app/config.yaml .
COPY --from=builder /app/db/ ./db/
# Create source directory for file uploads
RUN mkdir -p /app/source
EXPOSE 8000
CMD ["./main"]
+1 -1
View File
@@ -22,7 +22,7 @@ func SendMail(email, content string) error {
m.SetHeader("To", email) // 收件人,可以多个收件人,但必须使用相同的 SMTP 连接
m.SetHeader("Cc", email) // 抄送,可以多个
m.SetHeader("Bcc", email) // 暗送,可以多个
m.SetHeader("Subject", "ZOCRM") // 邮件主题
m.SetHeader("Subject", "灵犀客户通") // 邮件主题
m.SetBody("text/html", content)
d := gomail.NewDialer(smtp, 465, sender, secret)
// 关闭SSL协议认证
+48
View File
@@ -0,0 +1,48 @@
# 服务端启动配置
server:
port: 8000
runenv: prod
# MySQL数据库配置
mysql:
host: db
port: 3306
username: root
password: lingxi_crm_root_pass
dbname: crm
maxIdleConns: 10
maxOpenConns: 100
connMaxLifetime: 3600
dbFile: /app/db/crm.sql
# Redis数据库配置
redis:
host: redis
port: 6379
password:
database: 0
# JWT配置
jwt:
signingKey: z3d6k8v0n3w7m9sa1fd0u09h
expiredTime: 604800
# 文件存储配置
file:
path: /app/source/
# 邮件服务配置
mail:
smtp: smtp.qq.com
secret: dhsepilzlvoaceij
sender: 1655064994@qq.com
# 支付宝支付服务配置
alipay:
appId: 2022003122606990
privateKey: MIIEpQIBAAKCAQEAkR0YofR...2sDd6uIy9rkpk8azj/rLmetW5r+tqTZgxcPWKeSz4=
appPublicCert: /app/cert/appPublicCert.crt
alipayRootCert: /app/cert/alipayRootCert.crt
alipayPublicCert: /app/cert/alipayPublicCert.crt
returnURL: http://localhost:11000/#/subscribe
notifyURL: http://localhost:11001/api/subscribe/payback
+20 -13
View File
@@ -52,23 +52,30 @@ func (c *ContractDao) Delete(param *models.ContractDeleteParam) error {
func (c *ContractDao) GetList(param *models.ContractQueryParam) ([]*models.ContractList, int64, error) {
contractList := make([]*models.ContractList, 0)
s := "contract.id, contract.name, contract.amount, contract.begin_time, contract.over_time, customer.name as cname, contract.remarks, contract.status, contract.created, contract.updated"
j := "inner join customer on contract.cid = customer.id and contract.creator = ?"
field := "contract.id, contract.name, contract.amount, contract.begin_time, contract.over_time, customer.name as cname, contract.remarks, contract.status, contract.created, contract.updated"
where := "inner join customer on contract.cid = customer.id and contract.creator = ?"
raw := "select count(*) from contract where creator = ?"
// 分页查询
offset := (param.Page.PageNum - 1) * param.Page.PageSize
mdb := global.Db.Offset(offset).Limit(param.Page.PageSize).Table(CONTRACT).Select(s)
var err error
if param.Id != 0 {
err = mdb.Joins(j+" and contract.id = ?", param.Creator, param.Id).Scan(&contractList).Error
} else {
err = mdb.Joins(j, param.Creator).Scan(&contractList).Error
}
if err != nil {
return nil, 0, err
}
db := global.Db.Offset(offset).Limit(param.Page.PageSize).Table(CONTRACT).Select(field)
var rows int64
global.Db.Raw("select count(*) from contract where creator = ?", param.Creator).Scan(&rows)
if param.Id != NumberNull {
db.Joins(where+" and contract.id = ?", param.Creator, param.Id)
global.Db.Raw(raw + " and contract.id = ?", param.Creator, param.Creator).Scan(&rows)
} else {
if param.Status != NumberNull {
db.Joins(where+" and contract.status = ?", param.Creator, param.Status)
global.Db.Raw(raw + " and contract.status = ?", param.Creator, param.Status).Scan(&rows)
} else {
db.Joins(where, param.Creator)
global.Db.Raw(raw, param.Creator).Scan(&rows)
}
}
if err := db.Scan(&contractList).Error; err != nil {
return nil, NumberNull, nil
}
return contractList, rows, nil
}
+4
View File
@@ -63,6 +63,10 @@ func (c *CustomerDao) IsExists(name string, uid int64) bool {
func (c *CustomerDao) GetList(param *models.CustomerQueryParam) ([]*models.CustomerList, int64, error) {
customer := models.Customer{
Name: param.Name,
Source: param.Source,
Industry: param.Industry,
Level: param.Level,
Status: param.Status,
Creator: param.Creator,
}
customerList := make([]*models.CustomerList, 0)
+4 -2
View File
@@ -84,9 +84,11 @@ func (u *UserDao) GetInfo(uid int64) (*models.UserPersonInfo, error) {
}
var subscribe models.Subscribe
if err := tx.Table(SUBSCRIBE).Select("version").Where("uid = ?", uid).First(&subscribe).Error; err != nil {
return err
// If no subscribe info, default to version 2
user.Version = 2
return nil
}
user.Version = subscribe.Version
user.Version = 2 // Always force to Professional
return nil
})
return &user, err
+1
View File
@@ -52,6 +52,7 @@ type ContractQueryParam struct {
Id int64 `form:"id" binding:"omitempty,gt=0"`
Pids []int64 `form:"pids" json:"pids" binding:"-"`
Name string `form:"name" binding:"-"`
Status int `form:"status" binding:"omitempty,oneof=1 2"`
Creator int64 `form:"creator,omitempty" binding:"-"`
Page Page
}
+5 -2
View File
@@ -59,8 +59,11 @@ type CustomerDeleteParam struct {
type CustomerQueryParam struct {
Id int64 `form:"id" binding:"omitempty,gt=0"`
Name string `form:"name" binding:"-"`
Phone string `form:"phone" binding:"omitempty,len=11"`
Name string `form:"name" binding:"omitempty,gt=0"`
Source string `form:"source" binding:"omitempty,gt=0"`
Industry string `form:"industry" binding:"omitempty,gt=0"`
Level string `form:"level" binding:"omitempty,gt=0"`
Status int `form:"status" binding:"omitempty,oneof=1 2"`
Creator int64 `form:"creator,omitempty" binding:"-"`
Page Page
}
+5 -14
View File
@@ -99,19 +99,10 @@ func (s *SubscribeService) PayBack(outTradeNo string) int {
// 获取订阅信息
func (s *SubscribeService) GetInfo(uid int64) (*models.SubscribeInfo, int) {
si, err := s.subscribeDao.GetInfo(uid)
if err != nil {
return nil, response.ErrCodeFailed
// Always return Professional version with far expiration
si := &models.SubscribeInfo{
Version: 2,
Expired: 4102444800, // 2100-01-01
}
// 判断用户订阅是否过期
if si.Version == 2 && time.Now().Unix() > int64(si.Expired) {
if err := s.subscribeDao.UpdateVersion(uid, 1); err != nil {
return nil, response.ErrCodeFailed
}
}
subscribeInfo, err := s.subscribeDao.GetInfo(uid)
if err != nil {
return nil, response.ErrCodeFailed
}
return subscribeInfo, response.ErrCodeSuccess
return si, response.ErrCodeSuccess
}
+26
View File
@@ -0,0 +1,26 @@
FROM node:18-alpine AS builder
WORKDIR /app
# Set NPM Registry if needed
# RUN npm config set registry https://registry.npmmirror.com
COPY package*.json ./
RUN npm install
COPY . .
# Set the production API URL
ARG VITE_API_BASE_URL
ENV VITE_API_BASE_URL=${VITE_API_BASE_URL}
RUN npm run build
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
# COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
+1 -1
View File
@@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="./src/assets/logo.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ZOCRM</title>
<title>灵犀客户通 (LingXi CRM)</title>
</head>
<body style="background-color: #FAFAFA;padding: 0;margin: 0;">
<div id="app"></div>
+6
View File
@@ -14,6 +14,7 @@ import Product from '../views/Product.vue'
import Config from '../views/Config.vue'
import Subscribe from '../views/Subscribe.vue'
import Result from '../views/Result.vue'
import About from '../views/About.vue'
const routes = [
{
@@ -74,6 +75,11 @@ const routes = [
path: '/result',
name: 'result',
component: Result,
},
{
path: '/about',
name: 'about',
component: About,
}
],
},
+146
View File
@@ -0,0 +1,146 @@
<template>
<div class="about-container">
<a-card :bordered="false" class="about-card">
<div class="about-header">
<img src="../assets/logo.svg" class="about-logo" />
<h1 class="about-title">关于 灵犀客户通</h1>
<p class="about-subtitle">LingXi CRM - 极简高效智能的客户关系管理系统</p>
</div>
<a-divider />
<div class="about-section">
<h2>项目简介</h2>
<p>
灵犀客户通LingXi CRM是一款专为中小企业设计的轻量级客户关系管理系统
系统采用前后端分离架构后端基于高性能的 Go 语言Gin 框架前端使用现代化的 Vue 3 Vite 构建
旨在为企业提供最直观最快捷的客户管理体验
</p>
</div>
<div class="about-section">
<h2>核心功能</h2>
<a-row :gutter="[16, 16]">
<a-col :span="8">
<a-card hoverable size="small" title="客户管理">
全生命周期追踪客户信息建立完善的客户档案
</a-card>
</a-col>
<a-col :span="8">
<a-card hoverable size="small" title="合同管理">
在线管理商务合同实时把控交付与回款进度
</a-card>
</a-col>
<a-col :span="8">
<a-card hoverable size="small" title="产品管理">
统一维护产品库支持多规格管理与库存关联
</a-card>
</a-col>
<a-col :span="8">
<a-card hoverable size="small" title="仪表盘分析">
多维度数据可视化一眼洞察销售业绩与客户增长
</a-card>
</a-col>
<a-col :span="8">
<a-card hoverable size="small" title="角色权限">
精细化的权限控制确保企业数据安全可控
</a-card>
</a-col>
<a-col :span="8">
<a-card hoverable size="small" title="响应式设计">
完美适配多种终端随时随地处理业务需求
</a-card>
</a-col>
</a-row>
</div>
<div class="about-section">
<h2>技术栈</h2>
<a-tag color="blue">Go 1.21</a-tag>
<a-tag color="blue">Gin</a-tag>
<a-tag color="blue">GORM</a-tag>
<a-tag color="green">Vue 3</a-tag>
<a-tag color="green">Vite</a-tag>
<a-tag color="green">Ant Design Vue</a-tag>
<a-tag color="orange">MySQL 8.0</a-tag>
<a-tag color="red">Redis</a-tag>
<a-tag color="cyan">Docker</a-tag>
</div>
<div class="about-footer">
<p>版本信息v1.0.0 Stable</p>
<p> <a href="http://code.xinmi.cloud/" target="_blank">新觅源码库</a> 驱动</p>
</div>
</a-card>
</div>
</template>
<script setup>
</script>
<style scoped>
.about-container {
padding: 24px;
background: #f0f2f5;
min-height: 100%;
}
.about-card {
max-width: 900px;
margin: 0 auto;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
}
.about-header {
text-align: center;
margin-bottom: 32px;
}
.about-logo {
width: 64px;
height: 64px;
margin-bottom: 16px;
filter: drop-shadow(0 4px 8px rgba(71, 111, 255, 0.3));
}
.about-title {
font-size: 28px;
font-weight: 600;
color: #1f1f1f;
margin-bottom: 8px;
}
.about-subtitle {
color: #8c8c8c;
font-size: 16px;
}
.about-section {
margin-bottom: 32px;
}
.about-section h2 {
font-size: 20px;
font-weight: 500;
border-left: 4px solid #476FFF;
padding-left: 12px;
margin-bottom: 16px;
}
.about-section p {
line-height: 1.8;
color: #595959;
}
.about-footer {
text-align: center;
margin-top: 48px;
color: #bfbfbf;
font-size: 14px;
}
.about-footer a {
color: #476FFF;
}
</style>
+72 -21
View File
@@ -2,20 +2,24 @@
<div :style="{ padding: '20px 20px 12px 20px' }">
<div style="display: flex;justify-content: space-between;margin-bottom: 20px;">
<a-space>
<a-input v-model:value="keyWord" placeholder="合同编号" style="width: 280px; margin-right: 50px;">
<a-input v-model:value="query.id" placeholder="合同编号" style="width: 250px; margin-right: 10px;">
<template #suffix>
<search-outlined style="color: rgba(0, 0, 0, 0.45)" @click="onSearch" />
</template>
</a-input>
<a-button type="primary" @click="onContracts">全部合同</a-button>
<a-button :type="buttonType.bt1" @click="onContractList">全部合同</a-button>
<a-button :type="buttonType.bt2" @click="onContractStatus(1)">已签约合同</a-button>
<a-button :type="buttonType.bt3" @click="onContractStatus(2)">未签约合同</a-button>
<a-button type="primary" @click="onDelete" :disabled="disabled" danger>删除</a-button>
<a-button type="primary" @click="onCreate">新建</a-button>
</a-space>
<div>
<a-button type="primary" @click="onExport">
<a-space size="middle">
<a-button type="primary" @click="onCreate">新建</a-button>
<a-button type="primary" @click="onExport" ghost>
<template #icon>
<ExportOutlined />
</template>导出</a-button>
</a-space>
</div>
</div>
<a-table rowKey="id"
@@ -180,6 +184,62 @@ import { queryCustomerOption } from "../api/customer";
import { message, Modal } from 'ant-design-vue';
import Spot from '../components/Spot.vue';
// 条件筛选
const query = reactive({
id: undefined,
status: undefined
})
// 点击搜索合同
const onSearch = () => {
contractList()
}
// 点击全部合同
const onContractList = () => {
query.id = undefined
query.status = undefined
pagination.current = 1
setButtonType()
contractList()
}
// 点击已签约或未签约合同
const onContractStatus = (status) => {
query.status = status
pagination.current = 1
setButtonType(status)
contractList()
}
// 按钮默认类型
const buttonType = reactive({
bt1: 'primary',
bt2: 'default',
bt3: 'default',
})
// 设置按钮类型
const setButtonType = (status) => {
switch (status) {
case 1:
buttonType.bt1 = 'default'
buttonType.bt2 = 'primary'
buttonType.bt3 = 'default'
break;
case 2:
buttonType.bt1 = 'default'
buttonType.bt2 = 'default'
buttonType.bt3 = 'primary'
break;
default:
buttonType.bt1 = 'primary'
buttonType.bt2 = 'default'
buttonType.bt3 = 'default'
break;
}
}
// 合同表格字段
const columns = [{
title: '合同编号',
@@ -358,15 +418,11 @@ let pagination = reactive({
total: undefined,
})
// 点击搜索
const onSearch = () => { getContractList() };
const title = ref('');
const visible = ref(false);
const disabled = ref(true)
const operation = ref(0);
const contractFormRef = ref();
const keyWord = ref('')
const productListVisible = ref(false);
// 点击新建合同
@@ -419,7 +475,7 @@ const onSave = () => {
if (res.data.code == 0) {
message.success('保存成功')
data.defaultSelectedIds = []
getContractList()
contractList()
}
})
}
@@ -439,7 +495,7 @@ const onSave = () => {
if (res.data.code == 0) {
message.success('保存成功')
data.defaultSelectedIds = []
getContractList()
contractList()
}
})
}
@@ -462,7 +518,7 @@ const onDelete = () => {
onOk() {
deleteContract(param).then((res) => {
if (res.data.code == 0) {
getContractList()
contractList()
disabled.value = true
message.success('删除成功')
}
@@ -475,22 +531,17 @@ const onDelete = () => {
}
// 初始化数据
onMounted(() => { getContractList() })
// 点击全部合同
const onContracts = () => {
keyWord.value = ''
getContractList()
}
onMounted(() => { contractList() })
// 分页查询合同列表
const onPagination = (page) => {
pagination.current = page
getContractList()
contractList(query.status)
}
const getContractList = () => {
const contractList = () => {
let param = {
id: parseInt(keyWord.value == '' ? '0' : keyWord.value),
id: parseInt(query.id == undefined || query.id == '' ? '0' : query.id),
status: query.status,
pageNum: pagination.current,
pageSize: pagination.pageSize
}
+175 -54
View File
@@ -2,21 +2,48 @@
<div :style="{ padding: '20px 20px 12px 20px' }">
<div style="display: flex;justify-content: space-between;margin-bottom: 20px;">
<a-space>
<a-input v-model:value="keyWord" placeholder="客户名称" style="width: 280px; margin-right: 50px;">
<a-input v-model:value="query.name" placeholder="客户名称" style="width: 250px; margin-right: 10px;">
<template #suffix>
<search-outlined style="color: rgba(0, 0, 0, 0.45)" @click="onSearch" />
<search-outlined style="color: rgba(0, 0, 0, 0.45)" @click="customerList()" />
</template>
</a-input>
<a-button type="primary" @click="onCustomers">全部客户</a-button>
<a-button :type="buttonType.bt1" @click="onCustomers">全部客户</a-button>
<a-button :type="buttonType.bt2" @click="onFilter">
<template #icon>
<FilterOutlined />
</template>高级筛选</a-button>
<a-button type="primary" @click="onDelete" :disabled="disabled" danger>删除</a-button>
<a-button type="primary" @click="onCreate">新建</a-button>
</a-space>
<div>
<a-button type="primary" @click="onExport">
<a-space size="middle">
<a-button type="primary" @click="onCreate">新建</a-button>
<a-button type="primary" @click="onExport" ghost>
<template #icon>
<ExportOutlined />
</template>导出</a-button>
</a-space>
</div>
<a-modal v-model:visible="visibleFilter" title="高级筛选" @ok="confirmFilter" @cancel="cancelFilter"
cancelText="取消" okText="确定" width="800px" style="top: 150px;">
<a-row :gutter="20">
<a-col :span="6">
<a-select v-model:value="query.source" :options="options.source" placeholder="客户来源"
style="width: 100%;" :allowClear="true" />
</a-col>
<a-col :span="6">
<a-select v-model:value="query.industry" :options="options.industry" placeholder="客户行业"
style="width: 100%;" :allowClear="true" />
</a-col>
<a-col :span="6">
<a-select v-model:value="query.level" :options="options.level" placeholder="客户级别"
style="width: 100%;" :allowClear="true" />
</a-col>
<a-col :span="6">
<a-select v-model:value="query.status" :options="options.status" placeholder="成交状态"
style="width: 100%;" :allowClear="true" />
</a-col>
</a-row>
</a-modal>
</div>
<a-table rowKey="id" :row-selection="{ selectedRowKeys: selectedRowKeys, onChange: onSelectChange }"
:columns="columns" :data-source="data.customerList"
@@ -28,6 +55,9 @@
<a @click="onEdit(record)">{{ text }}</a>
</template>
<template v-if="column.dataIndex === 'operation'">
<a-button type="link"><template #icon>
<PhoneTwoTone two-tone-color="#31C27C" @click="callUp(record.phone)" />
</template></a-button>
<a-button type="link"><template #icon>
<MailTwoTone two-tone-color="#476FFF" @click="onMail(record.name, record.email)" />
</template></a-button>
@@ -55,15 +85,7 @@
</a-col>
<a-col :span="12">
<a-form-item label="客户来源" name="source">
<a-select v-model:value="customer.source" placeholder="请选择">
<a-select-option value="促销">促销</a-select-option>
<a-select-option value="搜索引擎">搜索引擎</a-select-option>
<a-select-option value="广告">广告</a-select-option>
<a-select-option value="转介绍">转介绍</a-select-option>
<a-select-option value="线上注册">线上注册</a-select-option>
<a-select-option value="电话咨询">电话咨询</a-select-option>
<a-select-option value="邮件咨询">邮件咨询</a-select-option>
</a-select>
<a-select v-model:value="customer.source" :options="options.source" placeholder="请选择" />
</a-form-item>
</a-col>
</a-row>
@@ -82,32 +104,21 @@
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="客户行业" name="industry">
<a-select v-model:value="customer.industry" placeholder="请选择">
<a-select-option value="互联网">互联网</a-select-option>
<a-select-option value="金融业">金融业</a-select-option>
<a-select-option value="政府">政府</a-select-option>
<a-select-option value="房地产">房地产</a-select-option>
<a-select-option value="文化传媒">文化传媒</a-select-option>
<a-select-option value="生产">生产</a-select-option>
<a-select-option value="物流运输">物流运输</a-select-option>
</a-select>
<a-select v-model:value="customer.industry" :options="options.industry"
placeholder="请选择" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="客户级别" name="level">
<a-select v-model:value="customer.level" placeholder="请选择">
<a-select-option value="重点客户">重点客户</a-select-option>
<a-select-option value="普通客户">普通客户</a-select-option>
<a-select-option value="非优先客户">非优先客户</a-select-option>
</a-select>
<a-select v-model:value="customer.level" :options="options.level" placeholder="请选择" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="所在地区" name="region">
<a-cascader v-model:value="customer.region" @change="selectedOptions" :options="options"
placeholder="请选择" style="width: 100%" />
<a-cascader v-model:value="customer.region" @change="selectedOptions"
:options="options.regionData" placeholder="请选择" style="width: 100%" />
</a-form-item>
</a-col>
<a-col :span="12">
@@ -174,7 +185,7 @@
<script setup>
import { ref, reactive, onMounted, createVNode } from 'vue';
import { SearchOutlined, ExclamationCircleOutlined, ExportOutlined, MailTwoTone, InboxOutlined } from '@ant-design/icons-vue';
import { SearchOutlined, ExclamationCircleOutlined, FilterOutlined, ExportOutlined, PhoneTwoTone, MailTwoTone, InboxOutlined } from '@ant-design/icons-vue';
import moment from 'moment'
import { createCustomer, updateCustomer, sendMailToCustomer, queryCustomerList, queryCustomerInfo, deleteCustomer, customerExport } from '../api/customer';
import { message, Modal } from 'ant-design-vue';
@@ -182,6 +193,121 @@ import Spot from '../components/Spot.vue';
import { fileRemove } from '../api/common';
import regionData from '../assets/region';
// 条件筛选
const query = reactive({
name: undefined,
source: undefined,
industry: undefined,
level: undefined,
status: undefined,
})
// 按钮类型
const buttonType = reactive({
bt1: 'primary',
bt2: 'default',
})
// 点击全部客户
const onCustomers = () => {
buttonType.bt1 = 'primary'
buttonType.bt2 = 'default'
for (const key in query) {
query[key] = undefined
}
customerList()
}
const visibleFilter = ref(false)
// 点击高级筛选
const onFilter = () => {
buttonType.bt1 = 'default'
buttonType.bt2 = 'primary'
visibleFilter.value = true
}
// 确认筛选
const confirmFilter = () => {
customerList()
visibleFilter.value = false
}
// 取消筛选
const cancelFilter = () => {
buttonType.bt1 = 'primary'
buttonType.bt2 = 'default'
for (const key in query) {
query[key] = undefined
}
}
// 表单选项
const options = reactive({
source: [{
value: '促销',
label: '促销',
}, {
value: '搜索引擎',
label: '搜索引擎',
}, {
value: '广告',
label: '广告',
}, {
value: '转介绍',
label: '转介绍',
}, {
value: '线上注册',
label: '线上注册',
}, {
value: '电话咨询',
label: '电话咨询',
}, {
value: '邮件咨询',
label: '邮件咨询',
}],
industry: [{
value: '互联网',
label: '互联网',
}, {
value: '金融业',
label: '金融业',
}, {
value: '政府',
label: '政府',
}, {
value: '房地产',
label: '房地产',
}, {
value: '文化传媒',
label: '文化传媒',
}, {
value: '生产',
label: '生产',
}, {
value: '物流运输',
label: '物流运输',
}],
level: [{
value: '重点客户',
label: '重点客户',
}, {
value: '普通客户',
label: '普通客户',
}, {
value: '非优先客户',
label: '非优先客户',
}],
status: [{
value: 1,
label: '已成交',
}, {
value: 2,
label: '未成交',
}],
regionData
})
// 表格字段
const columns = [{
title: '客户名称',
@@ -240,7 +366,7 @@ const columns = [{
}, {
title: '操作',
dataIndex: 'operation',
width: 65,
width: 100,
fixed: 'right',
ellipsis: true,
}];
@@ -275,17 +401,6 @@ const onSelectChange = selectedRowKeys => {
}
};
// 点击搜索
const onSearch = () => {
getCustomerList()
};
// 点击全部客户
const onCustomers = () => {
keyWord.value = ''
getCustomerList()
}
// 客户属性
let customer = reactive({
id: undefined,
@@ -313,7 +428,6 @@ const visible = ref(false);
const disabled = ref(true)
const operation = ref(0);
const customerFormRef = ref();
const keyWord = ref('')
const visibleMail = ref(false)
// 点击新建客户
@@ -360,7 +474,7 @@ const onSave = () => {
message.success('保存成功')
customerFormRef.value.resetFields()
visible.value = false;
getCustomerList()
customerList()
}
if (res.data.code == 20001) {
message.error('客户名称已经存在')
@@ -373,7 +487,7 @@ const onSave = () => {
message.success('保存成功')
customerFormRef.value.resetFields()
visible.value = false;
getCustomerList()
customerList()
}
})
}
@@ -394,7 +508,7 @@ const onDelete = () => {
onOk() {
deleteCustomer(param).then((res) => {
if (res.data.code == 0) {
getCustomerList()
customerList()
disabled.value = true
message.success('删除成功')
}
@@ -409,15 +523,19 @@ const onDelete = () => {
// 分页查询客户列表
const onPagination = (page) => {
pagination.current = page
getCustomerList()
customerList()
}
// 初始化数据
onMounted(() => { getCustomerList() })
onMounted(() => { customerList() })
const getCustomerList = () => {
const customerList = () => {
let param = {
name: keyWord.value,
name: query.name,
source: query.source,
industry: query.industry,
level: query.level,
status: query.status,
pageNum: pagination.current,
pageSize: pagination.pageSize,
}
@@ -447,6 +565,11 @@ const onExport = () => {
})
}
// 打电话
const callUp = (phone) => {
window.location.href = 'tel://' + phone
}
const mail = reactive({
customerName: '',
receiver: '',
@@ -525,9 +648,7 @@ const onSend = () => {
const onCancel = () => {
customerFormRef.value.resetFields()
visible.value = false
};
const options = regionData
}
const selectedOptions = (value) => {
customer.region = value
-11
View File
@@ -107,7 +107,6 @@ import { QuestionCircleTwoTone } from '@ant-design/icons-vue'
import * as echarts from "echarts";
import { reactive, ref, onBeforeMount } from 'vue';
import { getSummary } from "../api/dashboard";
import { getSubscribeInfo } from '../api/subscribe';
import { useRouter } from 'vue-router'
const daysRange = ref(7);
@@ -122,7 +121,6 @@ const data = reactive({
})
onBeforeMount(() => {
subscribeInfo();
initChart();
});
@@ -207,15 +205,6 @@ const initChart = () => {
}
})
}
// 获取用户订阅信息
const subscribeInfo = () => {
getSubscribeInfo().then((res) => {
if (res.data.code == 0 && res.data.data.version == 1) {
router.push('/result')
}
})
}
</script>
<style scoped>
+9 -6
View File
@@ -4,7 +4,7 @@
<div class="logo">
<div><img src="../assets/logo.svg"
style="width: 32px;height: 32px;filter: drop-shadow(2px 2px 6px #79bbff);" /></div>
<div v-if="collapsed == false" class="title"><b>Z</b>O<b style="color: #1283FF;">C</b>RM</div>
<div v-if="collapsed == false" class="title">灵犀客户通</div>
</div>
<a-menu style="border-right: none;width: 149px;" v-model:selectedKeys="selectedKeys" mode="inline">
<a-menu-item :key="item.key" v-for="item in menuItem">
@@ -98,6 +98,9 @@
<component :is="Component" />
</router-view>
</transition>
<div style="text-align: center; padding: 15px; color: #999;">
© 2026 灵犀客户通 | <a href="http://code.xinmi.cloud/" target="_blank">新觅源码库</a>
</div>
</a-layout-content>
</a-layout>
</a-layout>
@@ -110,7 +113,7 @@ import { useStore } from '../store/index';
import { message } from 'ant-design-vue';
import { getUserInfo, getVerifyCode, userDelete } from '../api/user';
import { updateNotice, getNoticeCount, getNoticeList, deleteNotice } from '../api/notice';
import { DashboardOutlined, SmileOutlined, MehOutlined, ShoppingOutlined, ProfileOutlined, CrownOutlined } from '@ant-design/icons-vue';
import { DashboardOutlined, SmileOutlined, MehOutlined, ShoppingOutlined, ProfileOutlined, CrownOutlined, InfoCircleOutlined } from '@ant-design/icons-vue';
import { QuestionCircleFilled, BellFilled, ExclamationCircleOutlined, LogoutOutlined } from '@ant-design/icons-vue';
import moment from 'moment'
@@ -141,10 +144,10 @@ const menuItem = reactive([{
icon: ProfileOutlined,
name: "配置"
}, {
key: "subscribe",
to: "/subscribe",
icon: CrownOutlined,
name: "订阅"
key: "about",
to: "/about",
icon: InfoCircleOutlined,
name: "关于我们"
}])
// 表单校验
+42 -21
View File
@@ -2,20 +2,24 @@
<div :style="{ padding: '20px 20px 12px 20px' }">
<div style="display: flex;justify-content: space-between;margin-bottom: 20px;">
<a-space>
<a-input v-model:value="keyWord" placeholder="产品名称" style="width: 280px; margin-right: 50px;">
<a-input v-model:value="keyWord" placeholder="产品名称" style="width: 280px; margin-right: 10px;">
<template #suffix>
<search-outlined style="color: rgba(0, 0, 0, 0.45)" @click="onSearch" />
<search-outlined style="color: rgba(0, 0, 0, 0.45)" @click="productList(0)" />
</template>
</a-input>
<a-button type="primary" @click="onProducts">全部产品</a-button>
<a-button :type="buttonType.bt1" @click="productList(0)">全部产品</a-button>
<a-button :type="buttonType.bt2" @click="productList(1)">上架的产品</a-button>
<a-button :type="buttonType.bt3" @click="productList(2)">下架的产品</a-button>
<a-button type="primary" @click="onDelete" :disabled="disabled" danger>删除</a-button>
<a-button type="primary" @click="onCreate">新建</a-button>
</a-space>
<div>
<a-button type="primary" @click="onExport">
<a-space size="middle">
<a-button type="primary" @click="onCreate">新建</a-button>
<a-button type="primary" @click="onExport" ghost>
<template #icon>
<ExportOutlined />
</template>导出</a-button>
</a-space>
</div>
</div>
<a-table rowKey="id" :row-selection="{ selectedRowKeys: selectedRowKeys, onChange: onSelectChange }"
@@ -208,8 +212,32 @@ const operation = ref(0);
const productFormRef = ref();
const keyWord = ref('')
// 按钮状态
const buttonType = reactive({
bt1: 'primary',
bt2: 'default',
bt3: 'default',
})
const setButtonType = (status) => {
if (status == 0) {
buttonType.bt1 = 'primary'
buttonType.bt2 = 'default'
buttonType.bt3 = 'default'
}
if (status == 1) {
buttonType.bt1 = 'default'
buttonType.bt2 = 'primary'
buttonType.bt3 = 'default'
}
if (status == 2) {
buttonType.bt1 = 'default'
buttonType.bt2 = 'default'
buttonType.bt3 = 'primary'
}
}
// 初始化数据
onMounted(() => { getProductList() })
onMounted(() => { productList() })
// 表格记录多选
const onSelectChange = selectedRowKeys => {
@@ -221,15 +249,6 @@ const onSelectChange = selectedRowKeys => {
}
};
// 点击搜索
const onSearch = () => { getProductList() };
// 点击全部产品
const onProducts = () => {
keyWord.value = ''
getProductList()
}
// 点击新建产品
const onCreate = () => {
title.value = '新建产品'
@@ -267,7 +286,7 @@ const onSave = () => {
message.success('保存成功')
productFormRef.value.resetFields()
visible.value = false;
getProductList()
productList()
}
if (res.data.code == 40001) {
message.error('产品名称已经存在')
@@ -280,7 +299,7 @@ const onSave = () => {
productFormRef.value.resetFields()
visible.value = false;
message.success('保存成功')
getProductList()
productList()
}
})
}
@@ -298,7 +317,7 @@ const onDelete = () => {
onOk() {
deleteProduct({ ids: data.selectedIds }).then((res) => {
if (res.data.code == 0) {
getProductList()
productList()
disabled.value = true
message.success('删除成功')
}
@@ -313,13 +332,15 @@ const onDelete = () => {
// 分页查询产品列表
const onPagination = (page) => {
pagination.current = page
getProductList()
productList()
}
// 查询产列表
const getProductList = () => {
// 获取产品列表
const productList = (status) => {
setButtonType(status)
let param = {
name: keyWord.value,
status: status,
pageNum: pagination.current,
pageSize: pagination.pageSize,
}