{ "version": "https://jsonfeed.org/version/1", "title": "涛声依旧", "subtitle": "天下事有难易乎?为之,则难者亦易矣", "icon": "https://hitoli.com/images/favicon.ico", "description": "天生我材必有用", "home_page_url": "https://hitoli.com", "items": [ { "id": "https://hitoli.com/2023/09/09/squid-stunnel-%E7%A7%91%E5%AD%A6%E4%B8%8A%E7%BD%91/", "url": "https://hitoli.com/2023/09/09/squid-stunnel-%E7%A7%91%E5%AD%A6%E4%B8%8A%E7%BD%91/", "title": "squid+stunnel 科学上网", "date_published": "2023-09-09T08:48:00.000Z", "content_html": "
科学上网的方法有多种,有很多第三方提供的免费方案,这些方案优缺点暂时不予讨论。实际工作生活中还是会有需要自己搭建的情况,这次介绍的是使用 squid+stunnel 方案进行搭建。
\n一台可以访问外网的服务器,如香港的云主机并安装 Ubuntu 系统。
\napt-get install -y squid |
生成用户文件
\napt-get install apache2-utils | |
htpasswd -c /etc/squid/squid_user.txt 用户名 |
修改 squid 配置
\n 1、直接修改 /etc/squid/squid.conf 文件
\n 2、修改 /etc/squid/conf.d/debian.conf 文件
\n两种方式都一样,在底部加入以下代码
#dns 服务器地址 | |
dns_nameservers 8.8.8.8 8.8.4.4 | |
dns_v4_first on | |
# 监听端口 | |
http_port 3128 | |
# 定义 squid 密码文件与 ncsa_auth 文件位置 | |
auth_param basic program /usr/lib/squid/basic_ncsa_auth /etc/squid/squid_user.txt | |
# 认证进程的数量 | |
auth_param basic children 15 | |
# 认证对话框显示提示信息 | |
auth_param basic realm Squid proxy-caching web server | |
# 认证有效期 | |
auth_param basic credentialsttl 24 hours | |
# 是否区分用户名大小,off 为不区分 | |
auth_param basic casesensitive off | |
# 对定义的 squid_user 文件内的用户开启认证访问 | |
acl 用户名 proxy_auth REQUIRED | |
# 允许 squid_user 文件内用户进行代理 | |
http_access allow 用户名 | |
# 顺序匹配,最后添加拒绝所有未允许的规则。不添加会发现,未匹配到的规则会被放行 | |
http_access deny all | |
# 缓存设置 | |
cache_dir ufs /var/spool/squid 100 16 256 read-only | |
cache_mem 0 MB | |
coredump_dir /var/spool/squid | |
# 配置高匿,不允许设置任何多余头信息,保持原请求 header。 | |
header_access Via deny all | |
header_access X-Forwarded-For deny all | |
header_access Server deny all | |
header_access X-Cache deny all | |
header_access X-Cache-Lookup deny all | |
forwarded_for off | |
via off | |
# logs 相关配置 | |
emulate_httpd_log on | |
logformat squid %{X-Forwarded-For}>h %ui %un [%tl] \"%rm %ru HTTP/%rv\" %Hs %<st \"%{Referer}>h\" \"%{User-Agent}>h\" %Ss:%Sh | |
access_log /var/log/squid/access.log squid | |
cache_log /var/log/squid/cache.log | |
cache_store_log /var/log/squid/store.log | |
logfile_rotate 20 |
至次已经可以通过填写安装 squid 的服务器 ip 加端口 3128 加用户名密码进行代理访问了(通过访问 https://www.ip.cn/ 查看 ip 就会发现自己的出口 ip 已经变成了 squid 服务器的 ip 了)。但是要想科学上网还必须对代理的数据进行加密,否则访问外网还是会被我国的长城防火墙阻挡,所以还需要安装 stunnel 来实现此目的。
\napt-get install -y stunnel |
openssl req -new -x509 -days 3650 -nodes -out stunnel.pem -keyout stunnel.pem |
3、将证书 stunnel.pem 放到 /etc/stunnel/ 目录下
\n4、修改 stunnel 配置 (/etc/stunnle/stunnle.conf)
\n; 设置stunnel的pid文件路径 | |
pid = /etc/stunnel/stunnel.pid | |
; 设置stunnel工作的用户(组) | |
setuid = root | |
setgid = root | |
; 开启日志等级:emerg (0), alert (1), crit (2), err (3), warning (4), notice (5), info (6), or debug (7) | |
debug = 7 | |
; 日志文件路径 | |
output = /etc/stunnel/stunnel.log | |
; 证书文件 | |
cert = /etc/stunnel/stunnel.pem | |
; 私钥文件 | |
key = /etc/stunnel/stunnel.pem | |
; 自定义服务名squid-proxy | |
[squid-proxy] | |
; 服务监听的端口,client要连接这个端口与server通信 | |
accept = 1234(自定义) | |
; 服务要连接的端口,连接到squid的3128端口,将数据发给squid | |
connect = 3128 |
可以安装在要代理的机器上,在需要代理的情况下再开启(代理地址填 127.0.0.1 加客户端监听端口)。也可以安装在国内的服务器上一直保持连接(代理信息填国内服务器 ip 加客户端监听端口)。本示例客户端为 windows 系统
\nhttps://www.stunnel.org/downloads.html
\n[squid-proxy] | |
client = yes | |
; 监听3128端口,那么用户浏览器的代理设置就是 stunnel-client-ip:3128 | |
accept = 3128 | |
; 要连接到的stunnel server的ip与端口 | |
connect = stunnel服务端ip:1234(服务端自定义端口) | |
; 需要验证对方发过来的证书 | |
verify = 2 | |
; 用来进行证书验证的文件(stunnel服务端生成的证书复制到以下目录并改名为stunnel-server.pem) | |
CAfile = C:\\Program Files (x86)\\stunnel\\config\\stunnel-server.pem |
至次配置好代理 ip 为 stunnel 客户端 ip 加端口 3128 就可以正式科学上网了。如果只想对需要科学的 url 进行代理,可以通过安装 Proxy SwitchyOmega 插件实现(规则地址可通过 https://github.com/gfwlist/gfwlist 获取)。
\n
\n
1、按【Win+X】
\n2、选择【终端管理员】
\n3、输入以下命令并回车:
\n reg add "HKCU\\Software\\Classes\\CLSID\\{86ca1aa0-34aa-4e8b-a509-50c905bae2a2}\\InprocServer32" /f /ve
4、重启电脑
\n", "tags": [ "Windows ", "Windows 11" ] }, { "id": "https://hitoli.com/2023/07/08/%E8%A7%A3%E5%86%B3Lombok%E6%8A%A5%E9%94%99/", "url": "https://hitoli.com/2023/07/08/%E8%A7%A3%E5%86%B3Lombok%E6%8A%A5%E9%94%99/", "title": "解决Lombok报错", "date_published": "2023-07-08T02:51:00.000Z", "content_html": "java: You aren’t using a compiler supported by lombok, so lombok will not work and has been disabled.
\nYour processor is: com.sun.proxy.$Proxy26
\nLombok supports: OpenJDK javac, ECJ
问题分析
\n属于 lombok 编译不通过,原因可能是因为依赖没有更到最新版本
解决办法
\n在 IntelliJ IDEA 的全局配置 Compiler 中添加如下配置:
-Djps.track.ap.dependencies=false |
该篇文章介绍了如何对 shoka 主题进行 jsdelivr 聚合拆分,以便使用国内镜像源和异步加载,从而优化网站速度。具体操作包括更改模板、注册 helper 和更改配置。其中,推荐使用 advVendors 配置,可自定义加载源和 js 文件名,同时支持异步加载、pjax 刷新和 integrity 防 XXS 等特性。
\n众所周知,jsdelivr 在国内的速度可以用慢的一批来形容而 shoka 主题使用了 jsdelivr 的 combine 功能加载第三方 js, 而 combine 在国内没有镜像源并且阻断了使用 CDN 并发加速的道路,本篇博文会将 jsdelivr 聚合拆分为几个独立的 js, 以便使用国内镜像源和异步加载。
\n此方案相较于本地化而言有较大速度优势,尤其在 CDN 并发加持下
\n打开 shoka\\layout_partials\\layout.njk,找到第 144 行左右:
\n<script src=\"https://cdn.polyfill.io/v3/polyfill.js\"></script> | |
{{ _vendor_js() }} | |
{{ _js('app.js') }} | |
{{ partial('_partials/third-party/baidu-analytics.njk', {}, {cache: true}) }} |
更改为如下内容:
\n<script src=\"https://cdn.polyfill.io/v3/polyfill.js\"></script> | |
{%- if theme.advVendors.enable %} | |
\t{% for i in _list_vendor_js() %} | |
\t\t{{ _adv_vendor_js(i) }} | |
\t{% endfor %} | |
{%- else %} | |
{{ _vendor_js() }} | |
{%- endif %} | |
{{ _js('app.js')}} | |
{{ partial('_partials/third-party/baidu-analytics.njk', {}, {cache: true}) }} |
打开 shoka\\scripts\\helpers\\asset.js, 最后一行新建空行,增加如下内容:
\nhexo.extend.helper.register('_list_vendor_js', () => { | |
return hexo.theme.config.vendorsList.js; | |
}); | |
hexo.extend.helper.register('_adv_vendor_js', function (js_name) { | |
const config = hexo.theme.config.advVendors.js[js_name]; | |
const src = config[\"src\"]; | |
let result; | |
if (src.indexOf(\"http\") !== -1) { | |
result = src; | |
} else if (src.indexOf(\"combine\") !== -1) { | |
console.log(\"The combine feature is not recommended!\") | |
result = hexo.theme.config.advVendors.combine + src; | |
} else if (src.indexOf(\"npm\") !== -1) { | |
result = hexo.theme.config.advVendors.npm + src.slice(4); | |
} else if (src.indexOf(\"gh\") !== -1) { | |
result = hexo.theme.config.advVendors.github + src.slice(3); | |
} else { | |
result = \"/\" + src; | |
} | |
let attr = {src: result}; | |
if (config[\"async\"]) attr[\"async\"] = \"async\"; | |
if (config[\"data-pjax\"]) attr[\"data-pjax\"] = \"data-pjax\"; | |
if (config[\"hash-value\"]) attr[\"integrity\"]=config[\"hash-value\"]; | |
if (config[\"deferLoad\"]) { | |
return htmlTag('script', {\"data-pjax\": true}, ` | |
const script=document.createElement(\"script\");script.src=\"${result}\",script.async=true,document.body.appendChild(script) | |
`) | |
} | |
return htmlTag('script', attr, ''); | |
}) |
在 shoka 目录下 _config.yml 增加如下内容:
\n推荐内容,可根据自己情况更改
\nadvVendors: | |
enable: true | |
github: \"https://cdn.jsdelivr.net/gh/\" | |
combine: \"https://cdn.jsdelivr.net/\" | |
npm: \"https://unpkg.com/\" | |
js: | |
pace: | |
src: https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-M/pace/1.0.2/pace.min.js | |
pjax: | |
src: https://lib.baomitu.com/pjax/0.2.8/pjax.min.js | |
fetch: | |
src: npm/whatwg-fetch@3.4.0/dist/fetch.umd.js | |
anime: | |
src: https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-M/animejs/3.2.0/anime.min.js | |
algolia: | |
src: https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-M/algoliasearch/4.12.1/algoliasearch-lite.umd.min.js | |
instantsearch: | |
src: https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-M/instantsearch.js/4.39.0/instantsearch.production.min.js | |
lazyload: | |
src: https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-M/lozad.js/1.16.0/lozad.min.js | |
quicklink: | |
src: https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-M/quicklink/2.2.0/quicklink.umd.min.js | |
fancybox: | |
src: https://lf26-cdn-tos.bytecdntp.com/cdn/expire-1-M/??jquery/3.5.1/jquery.min.js,fancybox/3.5.7/jquery.fancybox.min.js,justifiedGallery/3.8.1/js/jquery.justifiedGallery.min.js | |
async: true | |
valine: | |
src: gh/amehime/MiniValine@4.2.2-beta10/dist/MiniValine.min.js | |
copy_tex: | |
src: https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-M/KaTeX/0.12.0/contrib/copy-tex.min.js | |
async: true | |
chart: | |
src: npm/frappe-charts@1.5.0/dist/frappe-charts.min.iife.js | |
vendorsList: | |
js: | |
- pace | |
- pjax | |
- fetch | |
- anime | |
- algolia | |
- instantsearch | |
- lazyload | |
- quicklink | |
- fancybox | |
- valine | |
- copy_tex | |
- chart |
下面为结构详解:
\nadvVendors: | |
\tenable: true #是否开启,关闭使用主题默认加载 | |
\tgithub: #github 使用的加载源,需要协议头和末尾斜杠 | |
\tcombine: #聚合 js 使用的加载源 (不建议使用) | |
\tnpm: #npm 的加载源 | |
\tjs: | |
\t\tjspackage: #js 名,可以与文件名不一致 | |
\t\t\tsrc: \"资源地址,详情见后面\" | |
\t\t\t# async: true 异步加载此 js | |
\t\t\t# data-pjax: true 在 pjax 加载时刷新此 js | |
\t\t\t# hash-value: 这个资源的 integrity 值,用于防 XXS | |
\t\t\t# deferLoad: true 使用动态 DOM 节点添加延迟 js 加载 (实验性) | |
vendorsList: | |
js: | |
\t- jspackage #与上方 jspackage 一致即可 |
资源地址格式如下:
\nhttps://example.com/xxx.js 使用 http (s) 地址加载 js
\ncombine/xxx.js,xxx.js 使用 jsdelivr 的 combine 功能加载 (不推荐)
\nnpm/xxx/xxx.js 使用 npm 源加载 js
\ngh/xxx/xxx.js 使用 gh 源加载 js
\nxxx.js 从本地加载 js
\n 优先级如下:
\nhttp>combine>npm>gh > 本地
http 请求发起后接收不到返回数据!!!【测试环境没出问题,发到正式环境就有问题】
\n项目中通过 restTemplate 发起请求:
\nlog.info(\"请求入参:{}\",JSON.toJSONString(request));// 打印日志 1 | |
// 配置 http 请求的连接超时时间和读取超时时间 | |
HttpsClientRequestFactory factory = new HttpsClientRequestFactory(); | |
factory.setConnectTimeout(60 * 1000); | |
factory.setReadTimeout(5 * 60 * 1000); | |
RestTemplate restTemplate = new RestTemplate(factory); | |
Result<InventoryResult> result = restTemplate.postForObject(address.concat(inventoryUrl), request, Result.class); | |
log.info(\"库存同步,返回数据: {}\", result);// 打印日志 2 |
http 请求入参:{data=[{ productStatus=10,skuCode=null}], messageId=ewpfpr1t6ey5r6qj0su0w1h6rt73hr,token=vgvU5EJKuZbuHii7WH6pTINp40ZRicaqLz4dq5P7L6pDzWir8EEGZhCKPucQjljsw69EHasEy+iJfdTofDg==}
\n打印日志 2 内容为:没有打印内容!!!
\n最后发现是因为测试环境中数据量较小,http 请求后,很快能得到相应,而正式环境数据量较大,没有及时得到响应,连接或者读取超时!!!
\n/** | |
* 兼容调 Https 接口 | |
*/ | |
public class HttpsClientRequestFactory extends SimpleClientHttpRequestFactory { | |
@Override | |
protected void prepareConnection(HttpURLConnection connection, String httpMethod) | |
throws IOException { | |
if (connection instanceof HttpsURLConnection) { | |
prepareHttpsConnection((HttpsURLConnection) connection); | |
} | |
super.prepareConnection(connection, httpMethod); | |
} | |
private void prepareHttpsConnection(HttpsURLConnection connection) { | |
connection.setHostnameVerifier(new SkipHostnameVerifier()); | |
try { | |
connection.setSSLSocketFactory(createSslSocketFactory()); | |
} | |
catch (Exception ex) { | |
// Ignore | |
} | |
} | |
private SSLSocketFactory createSslSocketFactory() throws Exception { | |
SSLContext context = SSLContext.getInstance(\"TLS\"); | |
context.init(null, new TrustManager[] { new SkipX509TrustManager() }, | |
new SecureRandom()); | |
return context.getSocketFactory(); | |
} | |
private class SkipHostnameVerifier implements HostnameVerifier { | |
@Override | |
public boolean verify(String s, SSLSession sslSession) { | |
return true; | |
} | |
} | |
private static class SkipX509TrustManager implements X509TrustManager { | |
@Override | |
public X509Certificate[] getAcceptedIssuers() { | |
return new X509Certificate[0]; | |
} | |
@Override | |
public void checkClientTrusted(X509Certificate[] chain, String authType) { | |
} | |
@Override | |
public void checkServerTrusted(X509Certificate[] chain, String authType) { | |
} | |
} | |
} |
// 配置 http 请求的连接超时时间和读取超时时间 | |
HttpsClientRequestFactory factory = new HttpsClientRequestFactory(); | |
factory.setConnectTimeout(60 * 1000); | |
factory.setReadTimeout(5 * 60 * 1000); | |
RestTemplate restTemplate = new RestTemplate(factory); | |
BaseResult<QueryInventoryResult> result = restTemplate.postForObject(address.concat(inventoryUrl), request, Result.class); |
@Configuration | |
public class RestConfig { | |
//60 * 1000 | |
@Value(\"${rest.connectTimeout:60000}\") | |
private int connectTimeout; | |
//5 * 60 * 1000 | |
@Value(\"${rest.readTimeout:300000}\") | |
private int readTimeout; | |
@Bean | |
public RestTemplate restTemplate() { | |
SimpleClientHttpRequestFactory simpleClientHttpRequestFactory = new SimpleClientHttpRequestFactory(); | |
simpleClientHttpRequestFactory.setConnectTimeout(connectTimeout); | |
simpleClientHttpRequestFactory.setReadTimeout(readTimeout); | |
RestTemplate restTemplate = new RestTemplate(simpleClientHttpRequestFactory); | |
return restTemplate; | |
} |
@Configuration | |
public class RestConfig { | |
//60 * 1000 | |
@Value(\"${rest.connectTimeout:60000}\") | |
private int connectTimeout; | |
//5 * 60 * 1000 | |
@Value(\"${rest.readTimeout:300000}\") | |
private int readTimeout; | |
@Value(\"${rest.connectionRequestTimeout:300000}\") | |
private int connectionRequestTimeout; | |
/** | |
* 使用 HttpComponentsClientHttpRequestFactory 创建 http 请求(推荐) | |
*/ | |
@Bean | |
public RestTemplate restTemplate() { | |
HttpComponentsClientHttpRequestFactory httpRequestFactory = new HttpComponentsClientHttpRequestFactory(); | |
httpRequestFactory.setConnectionRequestTimeout(connectionRequestTimeout); | |
httpRequestFactory.setConnectTimeout(connectTimeout); | |
httpRequestFactory.setReadTimeout(readTimeout); | |
return new RestTemplate(httpRequestFactory); | |
} | |
} |
@Configuration | |
public class RestConfig { | |
/** | |
* 高并发采用 HttpClient 连接池 | |
*/ | |
@Bean | |
public RestTemplate restTemplate() { | |
return new RestTemplate(httpRequestFactory()); | |
} | |
@Bean | |
public ClientHttpRequestFactory httpRequestFactory() { | |
return new HttpComponentsClientHttpRequestFactory(httpClient()); | |
} | |
@Bean | |
public HttpClient httpClient() { | |
Registry<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory>create() | |
.register(\"http\", PlainConnectionSocketFactory.getSocketFactory()) | |
.register(\"https\", SSLConnectionSocketFactory.getSocketFactory()) | |
.build(); | |
PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(registry); | |
// 设置整个连接池最大连接数 | |
connectionManager.setMaxTotal(1000); | |
\t\t\t// 路由是对 maxTotal 的细分 | |
connectionManager.setDefaultMaxPerRoute(100); | |
// 定义不活动的时间(毫秒),超过的连接从连接池拿取需要重新验证 | |
connectionManager.setValidateAfterInactivity(200); | |
RequestConfig requestConfig = RequestConfig.custom() | |
.setSocketTimeout(30000) // 返回数据的超时时间 | |
.setConnectTimeout(20000) // 连接上服务器的超时时间 | |
.setConnectionRequestTimeout(1000) // 从连接池中获取连接的超时时间 | |
.build(); | |
return HttpClientBuilder.create() | |
.setDefaultRequestConfig(requestConfig) | |
.setConnectionManager(connectionManager) | |
.evictIdleConnections(2, TimeUnit.SECONDS) // 保持空闲的最大时间 | |
.build(); | |
} | |
} |
当我们迁移到 MySQL 5.7+ 的版本时,常会碰到 ERROR 1055 only_full_group_by 错误,这是 5.7 之后 SQL_MODE 默认打开了严格模式导致的错误。说明你代码里有地方写的不严谨。
\n#查询 sql_mode | |
select @@GLOBAL.sql_mode; | |
#删除 ONLY_FULL_GROUP_BY | |
#设置 sql_mode | |
set @@GLOBAL.sql_mode='STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION'; |
业务要求:查询所有省份:
\n
SELECT | |
\tDISTINCT(province_code), | |
\tprovince_name | |
FROM | |
\tt_mip_base_area |
SELECT | |
\tprovince_code, | |
\tany_value(province_name) | |
FROM t_mip_base_area | |
GROUP BY province_code |
SELECT | |
province_code, | |
province_name | |
FROM t_mip_base_area | |
GROUP BY province_code |
则会报错
\n
MySQL5.7 之后,sql_mode 中 ONLY_FULL_GROUP_BY 模式默认设置为打开状态。
\nONLY_FULL_GROUP_BY 的语义就是确定 select target list 中的所有列的值都是明确语义,简单的说来,在此模式下,target list 中的值要么是来自于聚合函数(sum、avg、max 等)的结果,要么是来自于 group by list 中的表达式的值
\nMySQL 提供了 any_value () 函数来抑制 ONLY_FULL_GROUP_BY 值被拒绝
\nany_value () 会选择被分到同一组的数据里第一条数据的指定列值作为返回数据
\n#windows 系统修复 | |
DISM.exe /Online /Cleanup-image /Scanhealth | |
DISM.exe /Online /Cleanup-image /Restorehealth | |
sfc /scannow | |
#windows 网络修复 | |
netsh winsock reset | |
#合并 ts 文件 | |
copy /b c:\\Users\\hito\\Downloads\\*.ts d:\\50.mp4 | |
#windows11 家庭版安装 Hyper-V(以下命令拷贝到 txt 文本后重命名为 bat 文件然后用管理员权限运行) | |
pushd \"%~dp0\" | |
dir /b %SystemRoot%\\servicing\\Packages\\*Hyper-V*.mum >> hyper-v.txt | |
for /f %%i in ('findstr /i . hyper-v.txt 2^>nul') do dism /online /norestart /add-package:\"%SystemRoot%\\servicing\\Packages\\%%i\" | |
del hyper-v.txt | |
Dism /online /enable-feature /featurename:Microsoft-Hyper-V-All /LimitAccess /ALL | |
#mysql 查看索引使用情况 | |
EXPLAIN SELECT A.XXX, B.XXX FROM TABLE1 A, TABLE2 B ON A.XXX = B.XXX; | |
#mysql 使用触发器插入数据自动生产 uuid 主键 | |
insert_before BEFORE 插入 | |
BEGIN | |
\t\t\tSET NEW.ID = REPLACE(UUID(),\"-\",\"\"); | |
END | |
#excel | |
#合并 | |
=CONCATENATE(\"EX010\",D2,C2,\"1\") | |
#截断 | |
=LEFT(E2,6) | |
#判断取值 | |
=IF(O2=\"宜都市\",\"420500000000000\",IF(O2=\"高新区\",\"420592000000000\",IF(O2=\"枝江市\",\"420583000000000\",IF(O2=\"当阳市\",\"420582000000000\",IF(O2=\"远安县\",\"420525000000000\",IF(O2=\"兴山县\",\"420526000000000\",IF(O2=\"秭归县\",\"420527000000000\",IF(O2=\"长阳\",\"420528000000000\",IF(O2=\"五峰县\",\"420529000000000\",IF(O2=\"夷陵区\",\"420506000000000\",IF(O2=\"点军区\",\"420504000000000\",IF(O2=\"西陵区\",\"420502000000000\",IF(OR(ISNUMBER(FIND({\"伍家\";\"伍家区\";\"伍家岗区\"},O2))),\"420503000000000\",IF(OR(ISNUMBER(FIND({\"猇亭\";\"猇亭区\"},O2))),\"420505000000000\")))))))))))))) | |
#经纬度计算 | |
=LEFT(T2,FIND(\"°\",T2)-1)+MID(T2,FIND(\"°\",T2)+1,FIND(\"′\",T2)-FIND(\"°\",T2)-1)/60+MID(T2,FIND(\"′\",T2)+1,FIND(\"″\",T2)-FIND(\"′\",T2)-1)/3600 |
{ | |
\"builder\": { | |
\"gc\": { | |
\"defaultKeepStorage\": \"20GB\", | |
\"enabled\": true | |
} | |
}, | |
\"experimental\": false, | |
\"features\": { | |
\"buildkit\": true | |
}, | |
\"insecure-registries\": [ | |
\"http://192.168.86.117\" | |
] | |
} |
\n5. 本项目 clean、compile、package
\n
\n6. 添加一个 DockerFile 的启动配置
\n 192.168.86.117/zhsl/ys-zhslsgcgl:0.0.1
\n
\n7. 保证代码是最新的之后,运行上步的配置,会把刚才 install 生成的 jar 包上传到本机 Docker 生成一个本机 docker 镜像
\n
\n 8. 命令行登录本地的 KUBESPHERE,把本机 docker 镜像上传到 KUBESPHERE
docker login 192.168.86.117 |
docker images命令也可以看到本机docker镜像 |
把本机docker镜像上传到KUBESPHERE | |
docker push 192.168.86.117/zhsl/ys-zhslsgcgl:0.0.1 |
\n9. 网页登录本地的 KUBESPHERE。
\nhttp://192.168.86.117:30880/,进入 “工作负载” 找到刚上传 Docker 服务镜像,并启动。
\n
注意:54XXXXXXXXC8 为光猫背后 ONU MAC:54-XX-XX-XX-XX-C8
\n获取telnet的用户名 | |
cfg_cmd get InternetGatewayDevice.DeviceInfo.X_CT-COM_ServiceManage.TelnetUserName | |
获取telnet的密码 | |
cfg_cmd get InternetGatewayDevice.DeviceInfo.X_CT-COM_ServiceManage.TelnetPassword |
su | |
密码:Fh@XXXXXX(X为mac地址的后6位) |
cfg_cmd get InternetGatewayDevice.DeviceInfo.X_CT-COM_TeleComAccount.Password | grep 'get success' | cut -d = -f 2 |
7、访问 http://192.168.1.1:8080/html/logoffaccount.html 把隐藏用户打开,其它选项改为可修改
\n两种命令模式:
\nload_cli factory |
show admin_pwd |
? |
show ? |
set ? |
show allinfo |
cfg_cmd set InternetGatewayDevice.DeviceInfo.X_CT-COM_ServiceManage.TelnetEnable 1 | |
cfg_cmd set InternetGatewayDevice.DeviceInfo.X_CT-COM_ServiceManage.TelnetUserName 用户名 | |
cfg_cmd set InternetGatewayDevice.DeviceInfo.X_CT-COM_ServiceManage.TelnetPassword 密码 |
cfg_cmd set InternetGatewayDevice.DeviceInfo.X_CT-COM_ServiceManage.FtpEnable 1 | |
cfg_cmd set InternetGatewayDevice.DeviceInfo.X_CT-COM_ServiceManage.FtpUserName 用户名 | |
cfg_cmd set InternetGatewayDevice.DeviceInfo.X_CT-COM_ServiceManage.FtpPassword 密码 |
cfg_cmd get InternetGatewayDevice.X_CT-COM_UserInfo.UserName |grep 'value' |cut -d '=' -f 2 |
cfg_cmd get InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.X_CT-COM_WANEponLinkConfig.Enable | |
cfg_cmd set InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.X_CT-COM_WANEponLinkConfig.Enable 0 #0 禁用,1 启用 | |
cfg_cmd get InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.X_CT-COM_WANEponLinkConfig.VLANIDMark | |
cfg_cmd set InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.X_CT-COM_WANEponLinkConfig.VLANIDMark 99 #默认 46 | |
cfg_cmd get InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.X_CT-COM_WANEponLinkConfig.Mode | |
cfg_cmd set InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.X_CT-COM_WANEponLinkConfig.Mode 2 #默认 2 | |
cfg_cmd get InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.X_CT-COM_AccessControl.Enabled | |
cfg_cmd set InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.X_CT-COM_AccessControl.Enabled 0 #0 禁用,1 启用 | |
cfg_cmd get InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANIPConnection.1.Enable | |
cfg_cmd set InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANIPConnection.1.Enable 0 #0 禁用,1 启用 |
cfg_cmd set InternetGatewayDevice.DeviceInfo.X_CT-COM_MiddlewareMgt.Tr069Enable 0 |
cfg_cmd set InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.X_CT-COM_WANEponLinkConfig.IP_Routed='' |
cfg_cmd get InternetGatewayDevice.Services.X_CT-COM_MWBAND.TotalTerminalNumber |
cfg_cmd set InternetGatewayDevice.Services.X_CT-COM_MWBAND.TotalTerminalNumber 255 |
cfg_cmd get InternetGatewayDevice.X_CT-COM_UserInfo.UserName |
cfg_cmd help |
cfg_cmd shownode InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1. [0|1] | |
(如cfg_cmd shownode InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1. [0]) |
网上搜索了几个修改 IDE 背景色和字体大小的方法,记录一下方便以后查看。
\n修改代码区颜色
\n
修改代码区字体大小
\n
修改控制台背景色
\n
修改控制台字体大小
\n
修改左侧背景色
\n
修改 UI 字体大小
\n
使用指定数据方式得到的线条总是不能达到想要的效果,一是阶梯线在价格发生变化的点存在倾斜,二是高低价的线很难完美显示,所以只能自己动手画了。代码仅供参考,如有错误的地方请指正!
\n
this.chart.addTechnicalIndicatorTemplate({ | |
\tname: 'BandHighLowInd', | |
shortName: '波段高低价', | |
calcParams: [{ value: 10 }], | |
precision: 2, | |
plots: [ | |
{ key: 'high', title: '高: ', color: (data, options) => { return options.line.colors[0] } }, | |
{ key: 'low', title: '低: ', color: (data, options) => { return options.line.colors[1] } }, | |
{ key: 'highLow', title: '高低: ', color: (data, options) => { return options.line.colors[2] } } | |
], | |
calcTechnicalIndicator: (dataList, { params }) => { | |
let compareKlineSize = params[0] // 取前后 k 的范围 | |
let highs = [] | |
let lows = [] | |
let highLows = [] | |
let findHigh // 高低价当前查找标识 | |
dataList.forEach(function (kLineData, i) { | |
let frontIndex = i - compareKlineSize | |
if (frontIndex <= 0) { | |
frontIndex = 0 | |
} | |
let frontDatas = [] | |
if (frontIndex < i) { | |
frontDatas = dataList.slice(frontIndex, i) | |
} | |
let afterIndex = i + compareKlineSize + 1 | |
let afterDatas = [] | |
if (i < dataList.length - 1) { | |
afterDatas = dataList.slice(i + 1, afterIndex) | |
} | |
if (frontDatas.length === compareKlineSize && afterDatas.length === compareKlineSize) { // 前后 K 线数据必须都找到要比较的条数 | |
let high = null | |
let low = null | |
frontDatas.concat(afterDatas).forEach(function (kLineData, i) { // 找出前后 k 线中的最高和最低价 | |
if (high === null || kLineData.high > high) { | |
high = kLineData.high | |
} | |
if (low === null || kLineData.low < low) { | |
low = kLineData.low | |
} | |
}) | |
if (kLineData.high > high) { // 当前 k 的高大于前后 k 中找出的高,则放入高价集合中 | |
highs.push({ | |
time: kLineData.timestamp, | |
value: kLineData.high, | |
index: i | |
}) | |
if (isNotEmpty(findHigh)) { | |
if (findHigh && highLows[highLows.length - 1].value !== kLineData.high) { | |
highLows.push({ | |
time: kLineData.timestamp, | |
value: kLineData.high, | |
index: i | |
}) | |
findHigh = false | |
} | |
} else { | |
highLows.push({ | |
time: kLineData.timestamp, | |
value: kLineData.high, | |
index: i | |
}) | |
findHigh = false | |
} | |
} | |
if (kLineData.low < low) { // 当前 k 的低小于前后 k 中找出的低,则放入低价集合中 | |
lows.push({ | |
time: kLineData.timestamp, | |
value: kLineData.low, | |
index: i | |
}) | |
if (isNotEmpty(findHigh)) { | |
if (!findHigh && highLows[highLows.length - 1].value !== kLineData.low) { | |
highLows.push({ | |
time: kLineData.timestamp, | |
value: kLineData.low, | |
index: i | |
}) | |
findHigh = true | |
} | |
} else { | |
highLows.push({ | |
time: kLineData.timestamp, | |
value: kLineData.low, | |
index: i | |
}) | |
findHigh = true | |
} | |
} | |
} | |
}) | |
let high, low, highLow | |
return dataList.map((kLineData, i) => { | |
let item = { | |
} | |
highs.forEach((data) => { | |
if (kLineData.timestamp === data.time) { | |
high = data.value | |
item.highOrigin = true | |
} | |
}) | |
if (isNotEmpty(high)) { // 持续先前的高,画出阶梯线 | |
item.timestamp = kLineData.timestamp | |
item.high = high | |
} | |
lows.forEach((data) => { | |
if (kLineData.timestamp === data.time) { | |
low = data.value | |
item.lowOrigin = true | |
} | |
}) | |
if (isNotEmpty(low)) { // 持续先前的低,画出阶梯线 | |
item.timestamp = kLineData.timestamp | |
item.low = low | |
} | |
highLows.forEach((data) => { | |
if (kLineData.timestamp === data.time) { | |
highLow = data.value | |
item.highLowOrigin = true | |
} | |
}) | |
if (isNotEmpty(highLow)) { // 持续先前的高低,方便 title 显示 | |
item.timestamp = kLineData.timestamp | |
item.highLow = highLow | |
} | |
return item | |
}) | |
}, | |
render: ({ ctx, dataSource, viewport, styles, xAxis, yAxis }) => { | |
if (dataSource.technicalIndicatorDataList.length <= 0) { | |
return | |
} | |
let x = xAxis.convertToPixel(0) | |
let high, low, highLow, highLowX | |
dataSource.technicalIndicatorDataList.forEach(function (data, i) { | |
// 画高线 | |
ctx.textBaseline = 'middle' | |
ctx.textAlign = 'center' | |
ctx.fillStyle = '#fff' | |
ctx.strokeStyle = '#fff' | |
if (styles.line && styles.line.colors && styles.line.colors[0]) { | |
ctx.fillStyle = styles.line.colors[0] | |
ctx.strokeStyle = styles.line.colors[0] | |
} | |
if (isNotEmpty(high) && data.high !== high) { | |
ctx.beginPath() | |
ctx.moveTo(x, yAxis.convertToPixel(high)) | |
ctx.lineTo(x, yAxis.convertToPixel(data.high)) | |
ctx.stroke() | |
ctx.closePath() | |
} | |
high = data.high | |
let y = yAxis.convertToPixel(high) | |
ctx.beginPath() | |
ctx.moveTo(x, y) | |
ctx.lineTo(x + viewport.dataSpace, y) | |
ctx.stroke() | |
ctx.closePath() | |
// 画低线 | |
ctx.fillStyle = '#fff' | |
ctx.strokeStyle = '#fff' | |
if (styles.line && styles.line.colors && styles.line.colors[1]) { | |
ctx.fillStyle = styles.line.colors[1] | |
ctx.strokeStyle = styles.line.colors[1] | |
} | |
if (isNotEmpty(low) && data.low !== low) { | |
ctx.beginPath() | |
ctx.moveTo(x, yAxis.convertToPixel(low)) | |
ctx.lineTo(x, yAxis.convertToPixel(data.low)) | |
ctx.stroke() | |
ctx.closePath() | |
} | |
low = data.low | |
y = yAxis.convertToPixel(low) | |
ctx.beginPath() | |
ctx.moveTo(x, y) | |
ctx.lineTo(x + viewport.dataSpace, y) | |
ctx.stroke() | |
ctx.closePath() | |
// 画高低线 | |
if (isNotEmpty(data.highLow) && data.highLowOrigin === true) { | |
if (isNotEmpty(highLow) && isNotEmpty(highLowX)) { | |
ctx.fillStyle = '#fff' | |
ctx.strokeStyle = '#fff' | |
if (styles.line && styles.line.colors && styles.line.colors[2]) { | |
ctx.fillStyle = styles.line.colors[2] | |
ctx.strokeStyle = styles.line.colors[2] | |
} | |
ctx.beginPath() | |
ctx.moveTo(highLowX, yAxis.convertToPixel(highLow)) | |
ctx.lineTo(x, yAxis.convertToPixel(data.highLow)) | |
ctx.stroke() | |
ctx.closePath() | |
} | |
highLow = data.highLow | |
highLowX = x | |
} | |
// 画价格 | |
if (data.high !== undefined && data.highOrigin === true) { // 画高价 | |
let text = Number.parseFloat(data.high).toFixed(2) | |
ctx.fillStyle = '#fff' | |
if (styles.line && styles.line.colors && styles.line.colors[0]) { | |
ctx.fillStyle = styles.line.colors[0] | |
} | |
let offset = 10 | |
let y = yAxis.convertToPixel(data.high) | |
for (let i = 2; i < offset - 1; i += 2) { | |
ctx.fillText('.', x, y - i) | |
} | |
ctx.fillText(text, x, y - offset) | |
} | |
if (data.low !== undefined && data.lowOrigin === true) { // 画低价 | |
let text = Number.parseFloat(data.low).toFixed(2) | |
ctx.fillStyle = '#fff' | |
if (styles.line && styles.line.colors && styles.line.colors[1]) { | |
ctx.fillStyle = styles.line.colors[1] | |
} | |
let offset = 15 | |
let y = yAxis.convertToPixel(data.low) | |
for (let i = 2; i < offset - 5; i += 2) { | |
ctx.fillText('.', x, y + i) | |
} | |
ctx.fillText(text, x, y + offset) | |
} | |
x += viewport.dataSpace | |
}) | |
} | |
}) |
在开发前端页面时会存在相同的页面展示,只是菜单名称不同、数据类型不同的情况。如果再拷贝一个页面来展示后期就需要维护两个页面,同时也增加了工作量。但是只使用一个页面通过传参来改变数据就方便多了。
\n{ | |
path: '/test/:type/:menuIndex', | |
name: 'Test', | |
component: _import('xxx/Test.vue') | |
} |
<template> | |
<el-menu | |
class=\"aside-menu\" | |
:default-active=\"menuIndex.toString()\" | |
@select=\"handleSelect\" | |
> | |
<el-menu-item | |
index=\"1\" | |
class=\"aside-menu-item\" | |
> | |
<template #title> | |
<span>TEST1</span> | |
<el-icon :size=\"20\"><TrendCharts /></el-icon> | |
</template> | |
</el-menu-item> | |
<el-menu-item | |
index=\"2\" | |
class=\"aside-menu-item\" | |
> | |
<template #title> | |
<span>TEST2</span> | |
<el-icon :size=\"20\"><Orange /></el-icon> | |
</template> | |
</el-menu-item> | |
</el-menu> | |
</template> | |
methods: { | |
handleSelect (val) { | |
if (val === '1') { | |
this.$router.push({ path: '/test/type1/' + val }) | |
} else if (val === '2') { | |
this.$router.push({ path: '/test/type2/' + val }) | |
} | |
} | |
} |
created () { | |
this.$watch( | |
() => this.$route.params, | |
(toParams, previousParams) => { | |
if (isNotEmpty(toParams.menuIndex)) { | |
this.menuIndex = toParams.menuIndex | |
} | |
if (isNotEmpty(toParams.type)) { | |
this.type = toParams.type | |
} | |
// 根据type查询数据 | |
... | |
} | |
) | |
if (isNotEmpty(this.$route.params.menuIndex)) { | |
this.menuIndex = this.$route.params.menuIndex | |
} | |
if (isNotEmpty(this.$route.params.type)) { | |
this.type = this.$route.params.type | |
} | |
}, | |
mounted () { | |
\t// 根据type查询数据 | |
\t... | |
} |
项目中需要画一幅收益概览图,研究了一下 Highcharts 的用法。特此收录方便以后再次使用时查阅。
\n
//x 轴时间集合 | |
var chartXAxisDateTimes = ['2022-01-01 00:00:00', '2022-02-01 00:00:00', '2022-03-01 00:00:00', '2022-04-01 00:00:00', '2022-05-01 00:00:00'] | |
//y 轴数据集合 | |
var chartYAxisDatas = [ | |
{ | |
title: '累计收益', | |
name: '我的策略累计收益', | |
type: 'spline', | |
unit: '%', | |
valueDecimals: 3, | |
height: '28%', | |
yAxisIndex: 0, //y 轴分区 index(从上到下,默认从 0 开始) | |
data: [{ | |
x: new Date('2022-01-01 00:00:00').getTime(), | |
y: 1, | |
color: '' | |
}, | |
{ | |
x: new Date('2022-02-01 00:00:00').getTime(), | |
y: 2, | |
color: '' | |
}, | |
{ | |
x: new Date('2022-03-01 00:00:00').getTime(), | |
y: 3, | |
color: '' | |
}, | |
{ | |
x: new Date('2022-04-01 00:00:00').getTime(), | |
y: 4, | |
color: '' | |
}, | |
{ | |
x: new Date('2022-05-01 00:00:00').getTime(), | |
y: 5, | |
color: '' | |
}] | |
}, | |
{ | |
title: '每月盈亏', | |
name: '当月盈利', | |
type: 'column', | |
unit: 'k', | |
valueDecimals: 0, | |
top: '34%', | |
height: '28%', | |
yAxisIndex: 1, //y 轴分区 index(从上到下,默认从 0 开始) | |
formatter: function () { | |
return this.value < 0 ? (-this.value) + 'k' : this.value + 'k' | |
}, | |
data: [{ | |
x: new Date('2022-01-01 00:00:00').getTime(), | |
y: 1, | |
color: '' | |
}, | |
{ | |
x: new Date('2022-02-01 00:00:00').getTime(), | |
y: 2, | |
color: '' | |
}, | |
{ | |
x: new Date('2022-03-01 00:00:00').getTime(), | |
y: 3, | |
color: '' | |
}, | |
{ | |
x: new Date('2022-04-01 00:00:00').getTime(), | |
y: 4, | |
color: '' | |
}, | |
{ | |
x: new Date('2022-05-01 00:00:00').getTime(), | |
y: 5, | |
color: '' | |
}] | |
}, | |
{ | |
title: '成交记录', | |
name: '当月买入', | |
type: 'column', | |
unit: 'k', | |
valueDecimals: 0, | |
top: '68%', | |
height: '28%', | |
yAxisIndex: 2, //y 轴分区 index(从上到下,默认从 0 开始) | |
data: [{ | |
x: new Date('2022-01-01 00:00:00').getTime(), | |
y: 1, | |
color: 'red' | |
}, | |
{ | |
x: new Date('2022-02-01 00:00:00').getTime(), | |
y: 2, | |
color: 'red' | |
}, | |
{ | |
x: new Date('2022-03-01 00:00:00').getTime(), | |
y: 3, | |
color: 'red' | |
}, | |
{ | |
x: new Date('2022-04-01 00:00:00').getTime(), | |
y: 4, | |
color: 'red' | |
}, | |
{ | |
x: new Date('2022-05-01 00:00:00').getTime(), | |
y: 5, | |
color: 'red' | |
}] | |
}, | |
{ | |
title: '成交记录', | |
name: '当月卖出', | |
type: 'column', | |
unit: 'k', | |
valueDecimals: 0, | |
top: '68%', | |
height: '28%', | |
yAxisIndex: 2, //y 轴分区 index(从上到下,默认从 0 开始) | |
data: [{ | |
x: new Date('2022-01-01 00:00:00').getTime(), | |
y: 1, | |
color: 'green' | |
}, | |
{ | |
x: new Date('2022-02-01 00:00:00').getTime(), | |
y: 2, | |
color: 'green' | |
}, | |
{ | |
x: new Date('2022-03-01 00:00:00').getTime(), | |
y: 3, | |
color: 'green' | |
}, | |
{ | |
x: new Date('2022-04-01 00:00:00').getTime(), | |
y: 4, | |
color: 'green' | |
}, | |
{ | |
x: new Date('2022-05-01 00:00:00').getTime(), | |
y: 5, | |
color: 'green' | |
}] | |
} | |
] | |
\t\t | |
// 最大回测范围 | |
var plotBandScope = [new Date('2022-02-01 00:00:00').getTime(), new Date('2022-04-01 00:00:00').getTime()] | |
// 图表配置 | |
var option = { | |
\tchart: { | |
\t height: 600, | |
\t type: 'spline' // 指定图表的类型,默认是折线图(line) | |
\t}, | |
\tplotOptions: { | |
\t spline: { | |
\t\tborderWidth: 0, | |
\t\tlineWidth: 1, | |
\t\tstates: { | |
\t\t hover: { | |
\t\t\tlineWidth: 2 | |
\t\t } | |
\t\t}, | |
\t\t//pointWidth: 4, // 柱子内宽度容 | |
\t\tdataLabels: { | |
\t\t style: { | |
\t\t\tfontSize: 11 | |
\t\t }, | |
\t\t enabled: false | |
\t\t}, | |
\t\tshowInLegend: true | |
\t }, | |
\t column: { | |
\t\tborderWidth: 0, | |
\t\tpointWidth: 4, // 柱子内宽度容 | |
\t\tdataLabels: { | |
\t\t style: { | |
\t\t\tfontSize: 11 | |
\t\t }, | |
\t\t enabled: false | |
\t\t}, | |
\t\tshowInLegend: true, | |
\t\tgrouping: true | |
\t } | |
\t}, | |
\trangeSelector: { | |
\t enabled: false | |
\t}, | |
\tcredits: { | |
\t enabled: false | |
\t}, | |
\t// legend: { | |
\t// enabled: true, | |
\t// verticalAlign: 'top' | |
\t// }, | |
\tnavigator: { // 导航器样式 | |
\t enabled: true, | |
\t height: 20 | |
\t}, | |
\tscrollbar: { // 滚动条样式 | |
\t barBackgroundColor: 'rgb(209, 218, 237)', | |
\t barBorderRadius: 0, | |
\t barBorderWidth: 0, | |
\t buttonBackgroundColor: 'rgb(242, 242, 242)', | |
\t buttonBorderWidth: 0, | |
\t buttonBorderRadius: 0, | |
\t trackBackgroundColor: 'none', | |
\t trackBorderWidth: 1, | |
\t trackBorderRadius: 0, | |
\t trackBorderColor: '#CCC' | |
\t}, | |
\txAxis: { | |
\t lineWidth: 1, | |
\t type: 'datetime', | |
\t categories: [], | |
\t dateTimeLabelFormats: { | |
\t\t// millisecond: '%H:%M:%S.%L', | |
\t\t// second: '%H:%M:%S', | |
\t\t// minute: '%H:%M', | |
\t\t// hour: '%m-%d %H:%M', | |
\t\tday: '%m-%d', | |
\t\tweek: '%m-%d', | |
\t\tmonth: '%Y-%m', | |
\t\tyear: '%Y' | |
\t }, | |
\t gridLineWidth: 1 | |
\t}, | |
\tyAxis: [], | |
\ttooltip: { | |
\t xDateFormat: '%Y-%m-%d %H:%M:%S', | |
\t style: { | |
\t\tfontSize: 16, | |
\t\tfontFamily: '微软雅黑', | |
\t\tfontWeight: 'normal', | |
\t\tcolor: '#666', | |
\t\tpadding: 10, | |
\t\tborderWidth: 0, | |
\t\tbackgroundColor: '#ddd' | |
\t }, | |
\t shared: true, | |
\t useHTML: true, | |
\t headerFormat: '<small style=\"font-size: 12px\">{point.key}</small><table>', // 标题格式, | |
\t pointFormat: '<tr><td><li style=\"color:{series.color};padding:0px\">{series.name}: </li>' + | |
\t '<div style=\"float: right;\">{point.y} {series.tooltip.valueDecimals}</div></td></tr>', | |
\t footerFormat: '</table>', | |
\t shadow: true, | |
\t followPointer: true // 跟随鼠标 | |
\t}, | |
\tseries: [] | |
} | |
// 根据最大回测范围修改图表配置 | |
if (plotBandScope && plotBandScope.length === 2) { | |
\toption.xAxis.plotBands = [{ | |
\t from: plotBandScope[0], | |
\t to: plotBandScope[1], | |
\t color: '#FCFFC5', | |
\t label: { | |
\t\ttext: '最大回测区间', | |
\t\talign: 'center', | |
\t\tverticalAlign: 'top', | |
\t\ty: 14 | |
\t } | |
\t}] | |
} | |
// 根据 x 轴时间集合修改图表配置 | |
if (chartXAxisDateTimes && chartXAxisDateTimes.length > 0) { | |
\toption.categories = chartXAxisDateTimes | |
} | |
// 根据 y 轴数据集合修改图表配置 | |
if (chartYAxisDatas && chartYAxisDatas.length > 0) { | |
\tchartYAxisDatas.forEach(function (item, i) { | |
\t if (item.type === 'spline' || item.type === 'line') { | |
\t\toption.yAxis.push({ | |
\t\t plotLines: [{ | |
\t\t\tcolor: 'black', // 线的颜色,定义为红色 | |
\t\t\tdashStyle: 'solid', // 默认值,这里定义为实线 | |
\t\t\tvalue: 0, // 定义在那个值上显示标示线,这里是在 x 轴上刻度为 3 的值处垂直化一条线 | |
\t\t\twidth: 2 // 标示线的宽度,2px | |
\t\t }], | |
\t\t tickAmount: 4, // 规定坐标轴上的刻度总数 | |
\t\t gridLineColor: '#ddd', | |
\t\t gridLineDashStyle: 'Solid', | |
\t\t labels: { // 坐标轴上刻度的样式及单位 | |
\t\t\talign: 'right', | |
\t\t\tx: 6, | |
\t\t\tstyle: { | |
\t\t\t color: '#999999', | |
\t\t\t fontSize: 12 | |
\t\t\t}, | |
\t\t\tformat: '{value}' + item.unit // 坐标轴上的单位 | |
\t\t }, | |
\t\t title: { | |
\t\t\ttext: item.title, | |
\t\t\tmargin: 20, | |
\t\t\tstyle: { | |
\t\t\t color: '#999999', | |
\t\t\t fontSize: 12, | |
\t\t\t align: 'middle' // 坐标轴对齐方式(相对于坐标轴的值) 默认是:middle 居中对齐 | |
\t\t\t} | |
\t\t }, | |
\t\t height: item.height, | |
\t\t lineWidth: 0 | |
\t\t}) | |
\t\toption.series.push({ | |
\t\t name: item.name, | |
\t\t data: item.data, | |
\t\t type: item.type, | |
\t\t color: 'rgb(170, 70, 67)', | |
\t\t tooltip: { | |
\t\t\tvalueSuffix: item.unit, // 数值后缀 | |
\t\t\tvalueDecimals: item.valueDecimals // 提示框数据精度 (保留小数点后 3 位) | |
\t\t }, | |
\t\t yAxis: item.yAxisIndex, | |
\t\t showInNavigator: false | |
\t\t}) | |
\t } else if (item.type === 'column') { | |
\t\toption.yAxis.push({ | |
\t\t type: item.type, | |
\t\t tickAmount: 4, // 规定坐标轴上的刻度总数 | |
\t\t startOnTick: true, | |
\t\t reversed: false, //y 轴刻度反转 | |
\t\t showFirstLabel: false, | |
\t\t showLastLabel: true, | |
\t\t gridLineColor: '#ddd', | |
\t\t gridLineDashStyle: 'Solid', | |
\t\t labels: { | |
\t\t\talign: 'right', | |
\t\t\tx: 12, | |
\t\t\tstyle: { | |
\t\t\t color: '#999999', | |
\t\t\t fontSize: 12 | |
\t\t\t}, | |
\t\t\tformatter: item.formatter, | |
\t\t\tformat: '{value}' + item.unit // 坐标轴上的单位 | |
\t\t }, | |
\t\t title: { | |
\t\t\ttext: item.title, | |
\t\t\tmargin: 20, | |
\t\t\tstyle: { | |
\t\t\t color: '#999999', | |
\t\t\t fontSize: 12, | |
\t\t\t align: 'middle' // 坐标轴对齐方式(相对于坐标轴的值) 默认是:middle 居中对齐 | |
\t\t\t} | |
\t\t }, | |
\t\t top: item.top, | |
\t\t height: item.height, | |
\t\t offset: 0, | |
\t\t lineWidth: 0 | |
\t\t}) | |
\t\toption.series.push({ | |
\t\t name: item.name, | |
\t\t type: item.type, | |
\t\t data: item.data, | |
\t\t tooltip: { | |
\t\t\tpointFormatter: function () { | |
\t\t\t return '<tr><td><li style=\"color: ' + this.color + ';padding:0px;\"></span> ' + item.name + | |
\t\t\t\t': </li><div style=\"float: right;\">' + this.y.toFixed(item.valueDecimals) + item.unit + '</div></td></tr>' | |
\t\t\t} | |
\t\t }, | |
\t\t yAxis: item.yAxisIndex, | |
\t\t showInNavigator: false | |
\t\t}) | |
\t } | |
\t}) | |
} | |
// 画图(container 为 div 的 id) | |
Highcharts.stockChart('container', option) |
最近重装了家里一台 mini 电脑,把系统升级成了最新的 win10 系统。但是使用起来却异常卡顿,查看任务管理器发现有个 wsappx 进程占用 cpu 严重。于是百度搜索得知它是微软商店的依赖进程,而我根本就用不上,所以直接禁用。
\n本次任务是需要在一个指标图上通过点击标记画出此标记参与计算的数据范围、最高最低值、参考线等等,于是有了以下代码。代码仅供参考,如有错误的地方请指正!
\n
// 箱体指标 | |
const boxDataScope = 300 // 箱体范围 | |
// 黄金线参数 | |
const goldenSectionA = 0.191 | |
const goldenSectionB = 0.382 | |
const goldenSectionC = 0.5 | |
const goldenSectionD = 0.618 | |
const goldenSectionE = 0.809 | |
this.chart.addTechnicalIndicatorTemplate({ | |
\tname: 'custom_box_solid', | |
shortName: '箱体', | |
precision: 2, | |
plots: [ //key 属性的值最好在主图数据范围内,否则 Y 轴的值会跟着变大可能会导致主图变成一条线 | |
{ key: 'max', title: '最高:' }, | |
{ key: 'min', title: '最低:' } | |
], | |
calcParams: [boxDataScope, goldenSectionA, goldenSectionB, goldenSectionC, goldenSectionD, goldenSectionE], | |
calcTechnicalIndicator: (dataList, { params, plots }) => { | |
let allDatas = [] // 所有数据 | |
let selectedDatas = [] // 选中的数据 | |
for (let i = 0; i < dataList.length; i++) { | |
let kLineData = dataList[i] | |
if (new Date(dateConvert(kLineData.timestamp)).getTime() === getGlobalObject('boxId')) { | |
const size = params[0] | |
// 找出当前数据往前的 size 条数据(包含自己) | |
let startData = i - size + 1 | |
if (startData < 0) { | |
startData = 0 | |
} | |
let endData = i + 1 | |
if (endData > dataList.length) { | |
endData = dataList.length | |
} | |
selectedDatas = dataList.slice(startData, endData) | |
} | |
allDatas.push({ | |
timestamp: kLineData.timestamp | |
}) | |
} | |
// 找出选中数据中最高最低差价 | |
let max, min | |
selectedDatas.forEach(function (item) { | |
let value = item.close - item.close2 | |
if (!max || value > max) { | |
max = value | |
} | |
if (!min || value < min) { | |
min = value | |
} | |
}) | |
\t // 返回指标最终数据(未选中的数据用空对象替换) | |
\t // 必须返回和 dataList 一样条数的数据,否则 title 不会显示 | |
return allDatas.map((data, i) => { | |
let item = { | |
} | |
selectedDatas.map((selected, j) => { | |
if (data.timestamp === selected.timestamp) { | |
item.timestamp = selected.timestamp | |
item.max = max | |
item.min = min | |
} | |
}) | |
return item | |
}) | |
}, | |
render: ({ ctx, dataSource, viewport, styles, xAxis, yAxis }) => { | |
if (dataSource.technicalIndicatorDataList.length <= 0) { // 无指标数据则不处理 | |
return | |
} | |
\t // X 轴起始像素 | |
let x = xAxis.convertToPixel(0) | |
// 标记选中数据的起止位置 | |
let start | |
let end = dataSource.technicalIndicatorDataList.length - 1 | |
dataSource.technicalIndicatorDataList.forEach(function (kLineData, i) { | |
if (kLineData.timestamp) { | |
if (!start) { | |
start = i | |
} | |
if (i < end && !dataSource.technicalIndicatorDataList[i + 1].timestamp) { | |
end = i | |
} | |
let max = kLineData.max | |
let min = kLineData.min | |
ctx.fillStyle = '#fff' | |
ctx.textBaseline = 'middle' | |
ctx.textAlign = 'center' | |
// 画箱体 | |
\t\t // 箱体颜色 | |
\t\t ctx.strokeStyle = '#DC143C' | |
\t\t //y 轴最高点位置 | |
\t\t let yHigh = yAxis.convertToPixel(max) | |
\t\t //y 轴最低点位置 | |
\t\t let yLow = yAxis.convertToPixel(min) | |
\t\t ctx.beginPath() | |
\t\t // 画笔移动到数据的 x 轴起始点,y 轴最高点 | |
\t\t ctx.moveTo(x, yHigh) | |
if (i === start) { // 如果是第一条数据则需要画一条竖线 | |
ctx.lineTo(x, yLow) // 画竖线 | |
ctx.moveTo(x, yHigh) // 画笔移回 | |
} | |
if (i === end) { // 如果是最后一条数据则需要画一条竖线 | |
ctx.lineTo(x, yLow) // 画竖线 | |
ctx.fillText(max, x + viewport.dataSpace, yHigh) // 标识箱体最高点的值 | |
ctx.moveTo(x, yLow) // 画笔移动到 Y 轴最低点 | |
ctx.fillText(min, x + viewport.dataSpace, yLow) // 标识箱体最低点的值 | |
} else { // 画两条横线,一条在 y 轴最高点,一条在 y 轴最低点 | |
ctx.lineTo(x + viewport.dataSpace, yHigh) //y 轴最高点横线 | |
ctx.moveTo(x, yLow) // 画笔移动到 y 轴最低点 | |
ctx.lineTo(x + viewport.dataSpace, yLow) //y 轴最低点横线 | |
} | |
ctx.stroke() | |
ctx.closePath() | |
// 画黄金线 | |
\t\t // 黄金线颜色 | |
ctx.strokeStyle = '#ffffff' | |
\t\t // 根据黄金线参数计算黄金线的值 | |
let goldenSectionLineA = (max - min) * goldenSectionA + min | |
let goldenSectionLineB = (max - min) * goldenSectionB + min | |
let goldenSectionLineC = (max - min) * goldenSectionC + min | |
let goldenSectionLineD = (max - min) * goldenSectionD + min | |
let goldenSectionLineE = (max - min) * goldenSectionE + min | |
\t\t // 根据黄金线的值获取 Y 轴高度 | |
let yA = yAxis.convertToPixel(goldenSectionLineA) | |
let yB = yAxis.convertToPixel(goldenSectionLineB) | |
let yC = yAxis.convertToPixel(goldenSectionLineC) | |
let yD = yAxis.convertToPixel(goldenSectionLineD) | |
let yE = yAxis.convertToPixel(goldenSectionLineE) | |
ctx.beginPath() | |
\t\t // 画第一条黄金线 | |
ctx.moveTo(x, yA) | |
ctx.lineTo(x + viewport.barSpace / 2, yA) | |
if (i === end) { // 是否最后一条数据,如果是则需要标识黄金线的值 | |
\t\t // 标识第一条黄金线的值 | |
ctx.fillText(goldenSectionLineA.toFixed(2), x + viewport.dataSpace, yA) | |
} | |
\t\t // 画第二条黄金线 | |
ctx.moveTo(x, yB) | |
ctx.lineTo(x + viewport.barSpace / 2, yB) | |
if (i === end) { | |
ctx.fillText(goldenSectionLineB.toFixed(2), x + viewport.dataSpace, yB) | |
} | |
\t\t // 画第三条黄金线 | |
ctx.moveTo(x, yC) | |
ctx.lineTo(x + viewport.barSpace / 2, yC) | |
if (i === end) { | |
ctx.fillText(goldenSectionLineC.toFixed(2), x + viewport.dataSpace, yC) | |
} | |
\t\t // 画第四条黄金线 | |
ctx.moveTo(x, yD) | |
ctx.lineTo(x + viewport.barSpace / 2, yD) | |
if (i === end) { | |
ctx.fillText(goldenSectionLineD.toFixed(2), x + viewport.dataSpace, yD) | |
} | |
\t\t // 画第五条黄金线 | |
ctx.moveTo(x, yE) | |
ctx.lineTo(x + viewport.barSpace / 2, yE) | |
if (i === end) { | |
ctx.fillText(goldenSectionLineE.toFixed(2), x + viewport.dataSpace, yE) | |
} | |
ctx.stroke() | |
ctx.closePath() | |
} | |
\t\t// 计算 X 轴的下一个位置 | |
x += viewport.dataSpace | |
}) | |
} | |
}) |
以上代码只是箱体的指标模版,还需要根据业务逻辑在标记上实现点击事件,然后通过事件动态添加移除箱体指标。
\nTengine 的性能和稳定性已经在大型的网站如淘宝网,天猫商城等得到了很好的检验。它的最终目标是打造一个高效、稳定、安全、易用的 Web 平台。从 2011 年 12 月开始,Tengine 成为一个开源项目。现在,它由 Tengine 团队开发和维护。Tengine 团队的核心成员来自于淘宝、搜狗等互联网企业。
\ntengine 简单来说就是淘宝自己基于 nginx 优化的网页引擎,在 nginx 原先基础上继续保持兼容,同时功能扩展,效率提高,可以看到目前淘宝网在这么多人同时使用的情况下依然稳定,我们足以相信 tengine,由于它是 nginx 的一个分生版本,所以几乎完全兼容 nginx,所以我认为 tengine 是搭建 lnmp 环境的不二之选。
\n首先访问 tengine 官方网站,获取最新的下载地址。
\n wget http://tengine.taobao.org/download/tengine-2.3.3.tar.gz
nginx -V
./configure --prefix=/usr/local/nginx --user=www --group=www --with-http_ssl_module --with-http_gzip_static_module --without-mail_pop3_module --without-mail_imap_module --without-mail_smtp_module --without-http_uwsgi_module --without-http_scgi_module
\n./configure 后面的参数是上一步获取的
make 或者 make -j 内核数
\n生成的文件在 objs 目录下
停止 nginx 服务 service nginx stop
\n 查看 nginx 目录 whereis nginx
\n 备份旧 nginx mv /usr/sbin/nginx /usr/sbin/nginx.old
\n 拷贝 objs 下的 nginx 替换旧 nginx cp ./objs/nginx /usr/sbin/
\n 备份旧 so 文件
\n拷贝 objs 下的 so 文件替换旧的 so 文件 cp ./objs/*.so /usr/lib/nginx/modules/
nginx -t
如果打印 test is successful 则表示替换成功。
\n然后执行 service nginx start 进行启动即可
由于有时候 nginx 代理的时候,第三方域名对应的 ip 可能发生变化,然而没有提前通知,然而如果不配置什么,nginx 又不能智能解析,因此 nginx 动态解析域名就比较重要。
\n该模块在启动 nginx 的时候会对域名进行一次解析,解析完成后,在 DNS 服务器设定的 TTL 过期时间内不会再次更新,在 TTL 过期后则会再次发起域名解析请求更新域名所对应的 IP 地址。
\n需要在 nginx 的配置文件中的 http 配置域内指定使用的 DNS 服务器,在 upstream 中需要进行域名解析的 server 后面添加 resolve 参数。
\n例:
\nhttp { | |
resolver ip; | |
upstream test { | |
server www.xxx.com:8080 resolve; | |
} | |
server { | |
listen 8080; | |
client_body_buffer_size 10m; | |
server_name localhost; | |
location / { | |
proxy_pass http://test; | |
} | |
} | |
} |
缺点:
\n1、每次解析域名之后,会从 DNS 服务器获取到改 DNS 的 TTL,在 TTL 期限内不会再次解析,所以如果目标域名发生改变,nginx 不会更新解析,知道 TTL 过期。
\n2、DNS 服务器在 http 配置域配置全,不能在 location 中细分指定。
\n在 http 配置域中配置 DNS 服务器,在 upstream 中按照这个模块的格式配置,支持设置每隔多少秒进行一次解析(抓包分析过设置 interval 可指定解析间隔),如果解析失败则使用缓存中的上一次解析结果的 IP 地址访问。
\n例:
\nhttp { | |
resolver ip; | |
upstream test { | |
jdomain www.xxx.com port=8080 interval=10; #指定域名和端口,每隔 10 秒进行一次解析 | |
} | |
server { | |
listen 8080; | |
client_body_buffer_size 10m; | |
server_name localhost; | |
location / { | |
proxy_pass http://test; | |
} | |
} | |
} |
缺点:DNS 服务器只能在 http 配域中全局配置
\n将域名置于变量中,在对 proxy_pass 进行转发的时候域名调用变量,可以按照 valid,设置的时间参数间隔地对变量中的域名进行解析。
\n例:
\nserver { | |
\tlisten 8080; | |
\tclient_body_buffer_size 10m; | |
\tserver_name localhost; | |
\tlocation / { | |
\t\t\tresolver ip valid=3s; | |
\t\t\tset $five \"www.xxx.com:8080\"; | |
\t\t\tproxy_pass http://${five}; | |
\t} | |
\t | |
} |
缺点:upstream 中不支持设置变量,因此后端有多台的时候该方案不可行。
\n在 upstream 中配置 dynamic_resolve,在 location 配置域中指定 NDS 服务器,按照 valid 设置的时间间隔地进行地址解析。只支持 http 模块的动态域名解析
\n例:
\nhttp { | |
upstream test { | |
dynamic_resolve fallback=stale fail_timeout=30s; | |
server www.xxx.com:8080; | |
} | |
server { | |
listen 8080; | |
location / { | |
resolver ip valid=3s; | |
proxy_pass http://test; | |
} | |
\t } | |
} |
缺点:需要将 nginx 的 bin 文件替换为 tengine 的 bin 文件。
\n", "tags": [ "Linux", "服务", "Nginx", "Nginx", "Tengine", "动态域名解析" ] }, { "id": "https://hitoli.com/2022/11/30/Nginx%E9%85%8D%E7%BD%AEstream%E8%B8%A9%E5%9D%91/", "url": "https://hitoli.com/2022/11/30/Nginx%E9%85%8D%E7%BD%AEstream%E8%B8%A9%E5%9D%91/", "title": "Nginx配置stream踩坑", "date_published": "2022-11-30T08:09:00.000Z", "content_html": "stream 模块一般用于 TCP/UDP 数据流的代理和负载均衡,可以通过 stream 模块代理转发 TCP 消息。我是用来转发 mysql、gitee 等连接的,有天 ip 发生了变动导致连接不上。前期试过配置 resolver 114.114.114.114 valid=60s; 来动态解析域名,结果 stream 模块不支持 set 函数,这就导致 ip 变动后必须手动重启或者 reload 一下 nginx 才能正常连接。后面经过搜索发现有人说用 Tengine 替代 nginx 可以实现就试了试,结果发现 Tengine 只能实现 http 下的动态域名解析,至此问题依旧。没办法,我只能通过定时任务加脚本判断 ip 是否变动,如果变动就 reload 一下 nginx。
\n#!/bin/bash | |
#使用 crontab -e 命令添加定时任务 */1 * * * * sh /home/xxx/autoReloadNginx.sh | |
home=\"/home/xxx\" #指定 home 路径,如果使用 `pwd` 则 domainIP.txt 生成在当前用户目录下 | |
domain=xxx.xxx.cn | |
IP=`ping -4 -c 4 $domain | grep from | tail -n 1 | awk -F ' ' '{print $4}'` | |
regex=\"\\b(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[1-9])\\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[1-9])\\b\" | |
if [ `echo $IP | egrep $regex | wc -l` -eq 1 ]; then | |
if [ ! -f \"$home/domainIP.txt\" ]; then | |
touch $home/domainIP.txt | |
echo $IP > $home/domainIP.txt | |
else | |
oldIP=`cat $home/domainIP.txt` | |
if [ \"$IP\" != \"$oldIP\" ]; then | |
nginx -s reload | |
echo $IP > $home/domainIP.txt | |
else | |
echo \"The domain name ip has not changed\" | |
fi | |
fi | |
else | |
echo \"The domain name resolution is incorrect\" | |
fi |
最近接到一个任务是通过 KLineChart✔️8.6.1 实现在一幅图上画两个合约的蜡烛图。研究 api 发现并没有通过配置实现的方法,于是联系作者沟通得知需要自己画图实现。于是有了本篇文章。
\n
let shortName = this.constant.periodTypeEnum.getNameByCode(this.klineType) + ' 合约2:' + this.currentInstrumentId | |
this.chart.addTechnicalIndicatorTemplate({ | |
name: 'custom_candle_solid', | |
shortName: shortName, | |
precision: 2, | |
bar: { | |
\tupColor: '#EF5350', | |
\tdownColor: '#26A69A', | |
\tnoChangeColor: '#888889' | |
}, | |
plots: [ | |
\t{ key: 'open', title: '开: ' }, | |
\t{ key: 'close', title: '收: ' }, | |
\t{ key: 'high', title: '高: ' }, | |
\t{ key: 'low', title: '低: ' } | |
], | |
calcTechnicalIndicator: (dataList, { params, plots }) => { | |
\treturn dataList.map((kLineData, i) => { | |
\t return { | |
\t\tinstrumentId: kLineData.instrumentId, | |
\t\ttimestamp: getDateTime(new Date(kLineData.timestamp)), | |
\t\topen: kLineData.open, | |
\t\tclose: kLineData.close, | |
\t\thigh: kLineData.high, | |
\t\tlow: kLineData.low | |
\t } | |
\t}) | |
}, | |
render: ({ ctx, dataSource, viewport, styles, xAxis, yAxis }) => { | |
\t// X 轴起始像素 | |
\tlet x = xAxis.convertToPixel(0) | |
\tdataSource.technicalIndicatorDataList.forEach(function (kLineData, i) { | |
\t let open = kLineData.open | |
\t let close = kLineData.close | |
\t let high = kLineData.high | |
\t let low = kLineData.low | |
\t // 给蜡烛柱设置颜色 | |
\t if (close > open) { // 涨 | |
\t\tctx.strokeStyle = '#EF5350' | |
\t\tctx.fillStyle = '#EF5350' | |
\t } else if (close < open) { // 跌 | |
\t\tctx.strokeStyle = '#26A69A' | |
\t\tctx.fillStyle = '#26A69A' | |
\t } else { // 未变动 | |
\t\tctx.strokeStyle = '#888889' | |
\t\tctx.fillStyle = '#888889' | |
\t } | |
\t // 获取开盘价 Y 轴像素 | |
\t let openY = yAxis.convertToPixel(open) | |
\t // 获取收盘价 Y 轴像素 | |
\t let closeY = yAxis.convertToPixel(close) | |
\t // 开、收、高、低的 Y 轴像素 | |
\t let priceY = [openY, closeY, yAxis.convertToPixel(high), yAxis.convertToPixel(low)] | |
\t // 从低到高排序 | |
\t priceY.sort(function (a, b) { | |
\t\treturn a - b | |
\t }) | |
\t // 画蜡烛柱上部 | |
\t ctx.fillRect(x - 0.5, priceY[0], 1, priceY[1] - priceY[0]) | |
\t // 画蜡烛柱下部 | |
\t ctx.fillRect(x - 0.5, priceY[2], 1, priceY[3] - priceY[2]) | |
\t // 蜡烛柱高度 | |
\t var barHeight = Math.max(1, priceY[2] - priceY[1]) | |
\t // 画蜡烛柱中部 (viewport.barSpace 蜡烛柱的宽度,随放大缩小操作而变化) | |
\t ctx.fillRect(x - (viewport.barSpace / 2), priceY[1], viewport.barSpace, barHeight) | |
\t // 下一蜡烛柱 X 轴的起始位置(viewport.dataSpace 蜡烛柱的宽度加蜡烛柱之间的间隔,随放大缩小操作而变化) | |
\t x += viewport.dataSpace | |
\t}) | |
} | |
}) |
this.chart.createTechnicalIndicator('custom_candle_solid', true, { id: 'paneId10', dragEnabled: true, height: this.height }) |