feat(data-tools): 新增入库类型选择功能并优化数据工具界面
- 在补数任务面板中添加入库类型单选按钮组,支持 MySQL 和 InfluxDB - 更新 AddData 接口定义,添加 StorageType 相关类型和选项接口 - 修改补数 API 请求逻辑,根据入库类型动态调整接口路径前缀 - 重构台账设备表单,统一使用装置网络参数作为 MAC 和 NDID 的单一数据源 - 优化台账线路表单,仅当存在 ID 时才设置 lineId 字段,避免空值传递 - 添加入库类型列表获取接口和相关数据处理逻辑 - 更新台账字典代码常量,新增终端型号字典码 - 优化台账树节点添加逻辑,增加前置条件验证和禁用原因提示 - 添加 InfluxDB 配置文件到额外资源目录 - 更新稳定数据分析视图,优化台账树数据结构处理和样式布局 - 完善 API 调试契约检查,确保设备和线路数据映射正确性 - 优化趋势查询性能,禁用全局加载状态提升用户体验
This commit is contained in:
BIN
build/extraResources/influxdb-1.7.0/influx.exe
Normal file
BIN
build/extraResources/influxdb-1.7.0/influx.exe
Normal file
Binary file not shown.
BIN
build/extraResources/influxdb-1.7.0/influx_inspect.exe
Normal file
BIN
build/extraResources/influxdb-1.7.0/influx_inspect.exe
Normal file
Binary file not shown.
BIN
build/extraResources/influxdb-1.7.0/influx_stress.exe
Normal file
BIN
build/extraResources/influxdb-1.7.0/influx_stress.exe
Normal file
Binary file not shown.
BIN
build/extraResources/influxdb-1.7.0/influx_tsm.exe
Normal file
BIN
build/extraResources/influxdb-1.7.0/influx_tsm.exe
Normal file
Binary file not shown.
BIN
build/extraResources/influxdb-1.7.0/influxd.exe
Normal file
BIN
build/extraResources/influxdb-1.7.0/influxd.exe
Normal file
Binary file not shown.
560
build/extraResources/influxdb-1.7.0/influxdb.conf
Normal file
560
build/extraResources/influxdb-1.7.0/influxdb.conf
Normal file
@@ -0,0 +1,560 @@
|
|||||||
|
### Welcome to the InfluxDB configuration file.
|
||||||
|
|
||||||
|
# The values in this file override the default values used by the system if
|
||||||
|
# a config option is not specified. The commented out lines are the configuration
|
||||||
|
# field and the default value used. Uncommenting a line and changing the value
|
||||||
|
# will change the value used at runtime when the process is restarted.
|
||||||
|
|
||||||
|
# Once every 24 hours InfluxDB will report usage data to usage.influxdata.com
|
||||||
|
# The data includes a random ID, os, arch, version, the number of series and other
|
||||||
|
# usage data. No data from user databases is ever transmitted.
|
||||||
|
# Change this option to true to disable reporting.
|
||||||
|
# reporting-disabled = false
|
||||||
|
|
||||||
|
# Bind address to use for the RPC service for backup and restore.
|
||||||
|
# bind-address = "127.0.0.1:8088"
|
||||||
|
|
||||||
|
###
|
||||||
|
### [meta]
|
||||||
|
###
|
||||||
|
### Controls the parameters for the Raft consensus group that stores metadata
|
||||||
|
### about the InfluxDB cluster.
|
||||||
|
###
|
||||||
|
|
||||||
|
[meta]
|
||||||
|
# Where the metadata/raft database is stored
|
||||||
|
dir = "/var/lib/influxdb/meta"
|
||||||
|
|
||||||
|
# Automatically create a default retention policy when creating a database.
|
||||||
|
# retention-autocreate = true
|
||||||
|
|
||||||
|
# If log messages are printed for the meta service
|
||||||
|
# logging-enabled = true
|
||||||
|
|
||||||
|
###
|
||||||
|
### [data]
|
||||||
|
###
|
||||||
|
### Controls where the actual shard data for InfluxDB lives and how it is
|
||||||
|
### flushed from the WAL. "dir" may need to be changed to a suitable place
|
||||||
|
### for your system, but the WAL settings are an advanced configuration. The
|
||||||
|
### defaults should work for most systems.
|
||||||
|
###
|
||||||
|
|
||||||
|
[data]
|
||||||
|
# The directory where the TSM storage engine stores TSM files.
|
||||||
|
dir = "/var/lib/influxdb/data"
|
||||||
|
|
||||||
|
# The directory where the TSM storage engine stores WAL files.
|
||||||
|
wal-dir = "/var/lib/influxdb/wal"
|
||||||
|
|
||||||
|
# The amount of time that a write will wait before fsyncing. A duration
|
||||||
|
# greater than 0 can be used to batch up multiple fsync calls. This is useful for slower
|
||||||
|
# disks or when WAL write contention is seen. A value of 0s fsyncs every write to the WAL.
|
||||||
|
# Values in the range of 0-100ms are recommended for non-SSD disks.
|
||||||
|
# wal-fsync-delay = "0s"
|
||||||
|
|
||||||
|
|
||||||
|
# The type of shard index to use for new shards. The default is an in-memory index that is
|
||||||
|
# recreated at startup. A value of "tsi1" will use a disk based index that supports higher
|
||||||
|
# cardinality datasets.
|
||||||
|
# index-version = "inmem"
|
||||||
|
|
||||||
|
# Trace logging provides more verbose output around the tsm engine. Turning
|
||||||
|
# this on can provide more useful output for debugging tsm engine issues.
|
||||||
|
# trace-logging-enabled = false
|
||||||
|
|
||||||
|
# Whether queries should be logged before execution. Very useful for troubleshooting, but will
|
||||||
|
# log any sensitive data contained within a query.
|
||||||
|
# query-log-enabled = true
|
||||||
|
|
||||||
|
# Validates incoming writes to ensure keys only have valid unicode characters.
|
||||||
|
# This setting will incur a small overhead because every key must be checked.
|
||||||
|
# validate-keys = false
|
||||||
|
|
||||||
|
# Settings for the TSM engine
|
||||||
|
|
||||||
|
# CacheMaxMemorySize is the maximum size a shard's cache can
|
||||||
|
# reach before it starts rejecting writes.
|
||||||
|
# Valid size suffixes are k, m, or g (case insensitive, 1024 = 1k).
|
||||||
|
# Values without a size suffix are in bytes.
|
||||||
|
# cache-max-memory-size = "1g"
|
||||||
|
|
||||||
|
# CacheSnapshotMemorySize is the size at which the engine will
|
||||||
|
# snapshot the cache and write it to a TSM file, freeing up memory
|
||||||
|
# Valid size suffixes are k, m, or g (case insensitive, 1024 = 1k).
|
||||||
|
# Values without a size suffix are in bytes.
|
||||||
|
# cache-snapshot-memory-size = "25m"
|
||||||
|
|
||||||
|
# CacheSnapshotWriteColdDuration is the length of time at
|
||||||
|
# which the engine will snapshot the cache and write it to
|
||||||
|
# a new TSM file if the shard hasn't received writes or deletes
|
||||||
|
# cache-snapshot-write-cold-duration = "10m"
|
||||||
|
|
||||||
|
# CompactFullWriteColdDuration is the duration at which the engine
|
||||||
|
# will compact all TSM files in a shard if it hasn't received a
|
||||||
|
# write or delete
|
||||||
|
# compact-full-write-cold-duration = "4h"
|
||||||
|
|
||||||
|
# The maximum number of concurrent full and level compactions that can run at one time. A
|
||||||
|
# value of 0 results in 50% of runtime.GOMAXPROCS(0) used at runtime. Any number greater
|
||||||
|
# than 0 limits compactions to that value. This setting does not apply
|
||||||
|
# to cache snapshotting.
|
||||||
|
# max-concurrent-compactions = 0
|
||||||
|
|
||||||
|
# CompactThroughput is the rate limit in bytes per second that we
|
||||||
|
# will allow TSM compactions to write to disk. Note that short bursts are allowed
|
||||||
|
# to happen at a possibly larger value, set by CompactThroughputBurst
|
||||||
|
# compact-throughput = "48m"
|
||||||
|
|
||||||
|
# CompactThroughputBurst is the rate limit in bytes per second that we
|
||||||
|
# will allow TSM compactions to write to disk.
|
||||||
|
# compact-throughput-burst = "48m"
|
||||||
|
|
||||||
|
# The threshold, in bytes, when an index write-ahead log file will compact
|
||||||
|
# into an index file. Lower sizes will cause log files to be compacted more
|
||||||
|
# quickly and result in lower heap usage at the expense of write throughput.
|
||||||
|
# Higher sizes will be compacted less frequently, store more series in-memory,
|
||||||
|
# and provide higher write throughput.
|
||||||
|
# Valid size suffixes are k, m, or g (case insensitive, 1024 = 1k).
|
||||||
|
# Values without a size suffix are in bytes.
|
||||||
|
# max-index-log-file-size = "1m"
|
||||||
|
|
||||||
|
# The maximum series allowed per database before writes are dropped. This limit can prevent
|
||||||
|
# high cardinality issues at the database level. This limit can be disabled by setting it to
|
||||||
|
# 0.
|
||||||
|
# max-series-per-database = 1000000
|
||||||
|
|
||||||
|
# The maximum number of tag values per tag that are allowed before writes are dropped. This limit
|
||||||
|
# can prevent high cardinality tag values from being written to a measurement. This limit can be
|
||||||
|
# disabled by setting it to 0.
|
||||||
|
# max-values-per-tag = 100000
|
||||||
|
|
||||||
|
# If true, then the mmap advise value MADV_WILLNEED will be provided to the kernel with respect to
|
||||||
|
# TSM files. This setting has been found to be problematic on some kernels, and defaults to off.
|
||||||
|
# It might help users who have slow disks in some cases.
|
||||||
|
# tsm-use-madv-willneed = false
|
||||||
|
|
||||||
|
###
|
||||||
|
### [coordinator]
|
||||||
|
###
|
||||||
|
### Controls the clustering service configuration.
|
||||||
|
###
|
||||||
|
|
||||||
|
[coordinator]
|
||||||
|
# The default time a write request will wait until a "timeout" error is returned to the caller.
|
||||||
|
# write-timeout = "10s"
|
||||||
|
|
||||||
|
# The maximum number of concurrent queries allowed to be executing at one time. If a query is
|
||||||
|
# executed and exceeds this limit, an error is returned to the caller. This limit can be disabled
|
||||||
|
# by setting it to 0.
|
||||||
|
# max-concurrent-queries = 0
|
||||||
|
|
||||||
|
# The maximum time a query will is allowed to execute before being killed by the system. This limit
|
||||||
|
# can help prevent run away queries. Setting the value to 0 disables the limit.
|
||||||
|
# query-timeout = "0s"
|
||||||
|
|
||||||
|
# The time threshold when a query will be logged as a slow query. This limit can be set to help
|
||||||
|
# discover slow or resource intensive queries. Setting the value to 0 disables the slow query logging.
|
||||||
|
# log-queries-after = "0s"
|
||||||
|
|
||||||
|
# The maximum number of points a SELECT can process. A value of 0 will make
|
||||||
|
# the maximum point count unlimited. This will only be checked every second so queries will not
|
||||||
|
# be aborted immediately when hitting the limit.
|
||||||
|
# max-select-point = 0
|
||||||
|
|
||||||
|
# The maximum number of series a SELECT can run. A value of 0 will make the maximum series
|
||||||
|
# count unlimited.
|
||||||
|
# max-select-series = 0
|
||||||
|
|
||||||
|
# The maxium number of group by time bucket a SELECT can create. A value of zero will max the maximum
|
||||||
|
# number of buckets unlimited.
|
||||||
|
# max-select-buckets = 0
|
||||||
|
|
||||||
|
###
|
||||||
|
### [retention]
|
||||||
|
###
|
||||||
|
### Controls the enforcement of retention policies for evicting old data.
|
||||||
|
###
|
||||||
|
|
||||||
|
[retention]
|
||||||
|
# Determines whether retention policy enforcement enabled.
|
||||||
|
# enabled = true
|
||||||
|
|
||||||
|
# The interval of time when retention policy enforcement checks run.
|
||||||
|
# check-interval = "30m"
|
||||||
|
|
||||||
|
###
|
||||||
|
### [shard-precreation]
|
||||||
|
###
|
||||||
|
### Controls the precreation of shards, so they are available before data arrives.
|
||||||
|
### Only shards that, after creation, will have both a start- and end-time in the
|
||||||
|
### future, will ever be created. Shards are never precreated that would be wholly
|
||||||
|
### or partially in the past.
|
||||||
|
|
||||||
|
[shard-precreation]
|
||||||
|
# Determines whether shard pre-creation service is enabled.
|
||||||
|
# enabled = true
|
||||||
|
|
||||||
|
# The interval of time when the check to pre-create new shards runs.
|
||||||
|
# check-interval = "10m"
|
||||||
|
|
||||||
|
# The default period ahead of the endtime of a shard group that its successor
|
||||||
|
# group is created.
|
||||||
|
# advance-period = "30m"
|
||||||
|
|
||||||
|
###
|
||||||
|
### Controls the system self-monitoring, statistics and diagnostics.
|
||||||
|
###
|
||||||
|
### The internal database for monitoring data is created automatically if
|
||||||
|
### if it does not already exist. The target retention within this database
|
||||||
|
### is called 'monitor' and is also created with a retention period of 7 days
|
||||||
|
### and a replication factor of 1, if it does not exist. In all cases the
|
||||||
|
### this retention policy is configured as the default for the database.
|
||||||
|
|
||||||
|
[monitor]
|
||||||
|
# Whether to record statistics internally.
|
||||||
|
# store-enabled = true
|
||||||
|
|
||||||
|
# The destination database for recorded statistics
|
||||||
|
# store-database = "_internal"
|
||||||
|
|
||||||
|
# The interval at which to record statistics
|
||||||
|
# store-interval = "10s"
|
||||||
|
|
||||||
|
###
|
||||||
|
### [http]
|
||||||
|
###
|
||||||
|
### Controls how the HTTP endpoints are configured. These are the primary
|
||||||
|
### mechanism for getting data into and out of InfluxDB.
|
||||||
|
###
|
||||||
|
|
||||||
|
[http]
|
||||||
|
# Determines whether HTTP endpoint is enabled.
|
||||||
|
# enabled = true
|
||||||
|
|
||||||
|
# Determines whether the Flux query endpoint is enabled.
|
||||||
|
# flux-enabled = false
|
||||||
|
|
||||||
|
# The bind address used by the HTTP service.
|
||||||
|
bind-address = ":18086"
|
||||||
|
|
||||||
|
# Determines whether user authentication is enabled over HTTP/HTTPS.
|
||||||
|
# auth-enabled = false
|
||||||
|
|
||||||
|
# The default realm sent back when issuing a basic auth challenge.
|
||||||
|
# realm = "InfluxDB"
|
||||||
|
|
||||||
|
# Determines whether HTTP request logging is enabled.
|
||||||
|
# log-enabled = true
|
||||||
|
|
||||||
|
# Determines whether the HTTP write request logs should be suppressed when the log is enabled.
|
||||||
|
# suppress-write-log = false
|
||||||
|
|
||||||
|
# When HTTP request logging is enabled, this option specifies the path where
|
||||||
|
# log entries should be written. If unspecified, the default is to write to stderr, which
|
||||||
|
# intermingles HTTP logs with internal InfluxDB logging.
|
||||||
|
#
|
||||||
|
# If influxd is unable to access the specified path, it will log an error and fall back to writing
|
||||||
|
# the request log to stderr.
|
||||||
|
# access-log-path = ""
|
||||||
|
|
||||||
|
# Filters which requests should be logged. Each filter is of the pattern NNN, NNX, or NXX where N is
|
||||||
|
# a number and X is a wildcard for any number. To filter all 5xx responses, use the string 5xx.
|
||||||
|
# If multiple filters are used, then only one has to match. The default is to have no filters which
|
||||||
|
# will cause every request to be printed.
|
||||||
|
# access-log-status-filters = []
|
||||||
|
|
||||||
|
# Determines whether detailed write logging is enabled.
|
||||||
|
# write-tracing = false
|
||||||
|
|
||||||
|
# Determines whether the pprof endpoint is enabled. This endpoint is used for
|
||||||
|
# troubleshooting and monitoring.
|
||||||
|
# pprof-enabled = true
|
||||||
|
|
||||||
|
# Enables a pprof endpoint that binds to localhost:6060 immediately on startup.
|
||||||
|
# This is only needed to debug startup issues.
|
||||||
|
# debug-pprof-enabled = false
|
||||||
|
|
||||||
|
# Determines whether HTTPS is enabled.
|
||||||
|
# https-enabled = false
|
||||||
|
|
||||||
|
# The SSL certificate to use when HTTPS is enabled.
|
||||||
|
# https-certificate = "/etc/ssl/influxdb.pem"
|
||||||
|
|
||||||
|
# Use a separate private key location.
|
||||||
|
# https-private-key = ""
|
||||||
|
|
||||||
|
# The JWT auth shared secret to validate requests using JSON web tokens.
|
||||||
|
# shared-secret = ""
|
||||||
|
|
||||||
|
# The default chunk size for result sets that should be chunked.
|
||||||
|
# max-row-limit = 0
|
||||||
|
|
||||||
|
# The maximum number of HTTP connections that may be open at once. New connections that
|
||||||
|
# would exceed this limit are dropped. Setting this value to 0 disables the limit.
|
||||||
|
# max-connection-limit = 0
|
||||||
|
|
||||||
|
# Enable http service over unix domain socket
|
||||||
|
# unix-socket-enabled = false
|
||||||
|
|
||||||
|
# The path of the unix domain socket.
|
||||||
|
# bind-socket = "/var/run/influxdb.sock"
|
||||||
|
|
||||||
|
# The maximum size of a client request body, in bytes. Setting this value to 0 disables the limit.
|
||||||
|
# max-body-size = 25000000
|
||||||
|
|
||||||
|
# The maximum number of writes processed concurrently.
|
||||||
|
# Setting this to 0 disables the limit.
|
||||||
|
# max-concurrent-write-limit = 0
|
||||||
|
|
||||||
|
# The maximum number of writes queued for processing.
|
||||||
|
# Setting this to 0 disables the limit.
|
||||||
|
# max-enqueued-write-limit = 0
|
||||||
|
|
||||||
|
# The maximum duration for a write to wait in the queue to be processed.
|
||||||
|
# Setting this to 0 or setting max-concurrent-write-limit to 0 disables the limit.
|
||||||
|
# enqueued-write-timeout = 0
|
||||||
|
|
||||||
|
###
|
||||||
|
### [logging]
|
||||||
|
###
|
||||||
|
### Controls how the logger emits logs to the output.
|
||||||
|
###
|
||||||
|
|
||||||
|
[logging]
|
||||||
|
# Determines which log encoder to use for logs. Available options
|
||||||
|
# are auto, logfmt, and json. auto will use a more a more user-friendly
|
||||||
|
# output format if the output terminal is a TTY, but the format is not as
|
||||||
|
# easily machine-readable. When the output is a non-TTY, auto will use
|
||||||
|
# logfmt.
|
||||||
|
# format = "auto"
|
||||||
|
|
||||||
|
# Determines which level of logs will be emitted. The available levels
|
||||||
|
# are error, warn, info, and debug. Logs that are equal to or above the
|
||||||
|
# specified level will be emitted.
|
||||||
|
# level = "info"
|
||||||
|
|
||||||
|
# Suppresses the logo output that is printed when the program is started.
|
||||||
|
# The logo is always suppressed if STDOUT is not a TTY.
|
||||||
|
# suppress-logo = false
|
||||||
|
|
||||||
|
###
|
||||||
|
### [subscriber]
|
||||||
|
###
|
||||||
|
### Controls the subscriptions, which can be used to fork a copy of all data
|
||||||
|
### received by the InfluxDB host.
|
||||||
|
###
|
||||||
|
|
||||||
|
[subscriber]
|
||||||
|
# Determines whether the subscriber service is enabled.
|
||||||
|
# enabled = true
|
||||||
|
|
||||||
|
# The default timeout for HTTP writes to subscribers.
|
||||||
|
# http-timeout = "30s"
|
||||||
|
|
||||||
|
# Allows insecure HTTPS connections to subscribers. This is useful when testing with self-
|
||||||
|
# signed certificates.
|
||||||
|
# insecure-skip-verify = false
|
||||||
|
|
||||||
|
# The path to the PEM encoded CA certs file. If the empty string, the default system certs will be used
|
||||||
|
# ca-certs = ""
|
||||||
|
|
||||||
|
# The number of writer goroutines processing the write channel.
|
||||||
|
# write-concurrency = 40
|
||||||
|
|
||||||
|
# The number of in-flight writes buffered in the write channel.
|
||||||
|
# write-buffer-size = 1000
|
||||||
|
|
||||||
|
|
||||||
|
###
|
||||||
|
### [[graphite]]
|
||||||
|
###
|
||||||
|
### Controls one or many listeners for Graphite data.
|
||||||
|
###
|
||||||
|
|
||||||
|
[[graphite]]
|
||||||
|
# Determines whether the graphite endpoint is enabled.
|
||||||
|
# enabled = false
|
||||||
|
# database = "graphite"
|
||||||
|
# retention-policy = ""
|
||||||
|
# bind-address = ":2003"
|
||||||
|
# protocol = "tcp"
|
||||||
|
# consistency-level = "one"
|
||||||
|
|
||||||
|
# These next lines control how batching works. You should have this enabled
|
||||||
|
# otherwise you could get dropped metrics or poor performance. Batching
|
||||||
|
# will buffer points in memory if you have many coming in.
|
||||||
|
|
||||||
|
# Flush if this many points get buffered
|
||||||
|
# batch-size = 5000
|
||||||
|
|
||||||
|
# number of batches that may be pending in memory
|
||||||
|
# batch-pending = 10
|
||||||
|
|
||||||
|
# Flush at least this often even if we haven't hit buffer limit
|
||||||
|
# batch-timeout = "1s"
|
||||||
|
|
||||||
|
# UDP Read buffer size, 0 means OS default. UDP listener will fail if set above OS max.
|
||||||
|
# udp-read-buffer = 0
|
||||||
|
|
||||||
|
### This string joins multiple matching 'measurement' values providing more control over the final measurement name.
|
||||||
|
# separator = "."
|
||||||
|
|
||||||
|
### Default tags that will be added to all metrics. These can be overridden at the template level
|
||||||
|
### or by tags extracted from metric
|
||||||
|
# tags = ["region=us-east", "zone=1c"]
|
||||||
|
|
||||||
|
### Each template line requires a template pattern. It can have an optional
|
||||||
|
### filter before the template and separated by spaces. It can also have optional extra
|
||||||
|
### tags following the template. Multiple tags should be separated by commas and no spaces
|
||||||
|
### similar to the line protocol format. There can be only one default template.
|
||||||
|
# templates = [
|
||||||
|
# "*.app env.service.resource.measurement",
|
||||||
|
# # Default template
|
||||||
|
# "server.*",
|
||||||
|
# ]
|
||||||
|
|
||||||
|
###
|
||||||
|
### [collectd]
|
||||||
|
###
|
||||||
|
### Controls one or many listeners for collectd data.
|
||||||
|
###
|
||||||
|
|
||||||
|
[[collectd]]
|
||||||
|
# enabled = false
|
||||||
|
# bind-address = ":25826"
|
||||||
|
# database = "collectd"
|
||||||
|
# retention-policy = ""
|
||||||
|
#
|
||||||
|
# The collectd service supports either scanning a directory for multiple types
|
||||||
|
# db files, or specifying a single db file.
|
||||||
|
# typesdb = "/usr/local/share/collectd"
|
||||||
|
#
|
||||||
|
# security-level = "none"
|
||||||
|
# auth-file = "/etc/collectd/auth_file"
|
||||||
|
|
||||||
|
# These next lines control how batching works. You should have this enabled
|
||||||
|
# otherwise you could get dropped metrics or poor performance. Batching
|
||||||
|
# will buffer points in memory if you have many coming in.
|
||||||
|
|
||||||
|
# Flush if this many points get buffered
|
||||||
|
# batch-size = 5000
|
||||||
|
|
||||||
|
# Number of batches that may be pending in memory
|
||||||
|
# batch-pending = 10
|
||||||
|
|
||||||
|
# Flush at least this often even if we haven't hit buffer limit
|
||||||
|
# batch-timeout = "10s"
|
||||||
|
|
||||||
|
# UDP Read buffer size, 0 means OS default. UDP listener will fail if set above OS max.
|
||||||
|
# read-buffer = 0
|
||||||
|
|
||||||
|
# Multi-value plugins can be handled two ways.
|
||||||
|
# "split" will parse and store the multi-value plugin data into separate measurements
|
||||||
|
# "join" will parse and store the multi-value plugin as a single multi-value measurement.
|
||||||
|
# "split" is the default behavior for backward compatability with previous versions of influxdb.
|
||||||
|
# parse-multivalue-plugin = "split"
|
||||||
|
###
|
||||||
|
### [opentsdb]
|
||||||
|
###
|
||||||
|
### Controls one or many listeners for OpenTSDB data.
|
||||||
|
###
|
||||||
|
|
||||||
|
[[opentsdb]]
|
||||||
|
# enabled = false
|
||||||
|
# bind-address = ":4242"
|
||||||
|
# database = "opentsdb"
|
||||||
|
# retention-policy = ""
|
||||||
|
# consistency-level = "one"
|
||||||
|
# tls-enabled = false
|
||||||
|
# certificate= "/etc/ssl/influxdb.pem"
|
||||||
|
|
||||||
|
# Log an error for every malformed point.
|
||||||
|
# log-point-errors = true
|
||||||
|
|
||||||
|
# These next lines control how batching works. You should have this enabled
|
||||||
|
# otherwise you could get dropped metrics or poor performance. Only points
|
||||||
|
# metrics received over the telnet protocol undergo batching.
|
||||||
|
|
||||||
|
# Flush if this many points get buffered
|
||||||
|
# batch-size = 1000
|
||||||
|
|
||||||
|
# Number of batches that may be pending in memory
|
||||||
|
# batch-pending = 5
|
||||||
|
|
||||||
|
# Flush at least this often even if we haven't hit buffer limit
|
||||||
|
# batch-timeout = "1s"
|
||||||
|
|
||||||
|
###
|
||||||
|
### [[udp]]
|
||||||
|
###
|
||||||
|
### Controls the listeners for InfluxDB line protocol data via UDP.
|
||||||
|
###
|
||||||
|
|
||||||
|
[[udp]]
|
||||||
|
# enabled = false
|
||||||
|
# bind-address = ":8089"
|
||||||
|
# database = "udp"
|
||||||
|
# retention-policy = ""
|
||||||
|
|
||||||
|
# InfluxDB precision for timestamps on received points ("" or "n", "u", "ms", "s", "m", "h")
|
||||||
|
# precision = ""
|
||||||
|
|
||||||
|
# These next lines control how batching works. You should have this enabled
|
||||||
|
# otherwise you could get dropped metrics or poor performance. Batching
|
||||||
|
# will buffer points in memory if you have many coming in.
|
||||||
|
|
||||||
|
# Flush if this many points get buffered
|
||||||
|
# batch-size = 5000
|
||||||
|
|
||||||
|
# Number of batches that may be pending in memory
|
||||||
|
# batch-pending = 10
|
||||||
|
|
||||||
|
# Will flush at least this often even if we haven't hit buffer limit
|
||||||
|
# batch-timeout = "1s"
|
||||||
|
|
||||||
|
# UDP Read buffer size, 0 means OS default. UDP listener will fail if set above OS max.
|
||||||
|
# read-buffer = 0
|
||||||
|
|
||||||
|
###
|
||||||
|
### [continuous_queries]
|
||||||
|
###
|
||||||
|
### Controls how continuous queries are run within InfluxDB.
|
||||||
|
###
|
||||||
|
|
||||||
|
[continuous_queries]
|
||||||
|
# Determines whether the continuous query service is enabled.
|
||||||
|
# enabled = true
|
||||||
|
|
||||||
|
# Controls whether queries are logged when executed by the CQ service.
|
||||||
|
# log-enabled = true
|
||||||
|
|
||||||
|
# Controls whether queries are logged to the self-monitoring data store.
|
||||||
|
# query-stats-enabled = false
|
||||||
|
|
||||||
|
# interval for how often continuous queries will be checked if they need to run
|
||||||
|
# run-interval = "1s"
|
||||||
|
|
||||||
|
###
|
||||||
|
### [tls]
|
||||||
|
###
|
||||||
|
### Global configuration settings for TLS in InfluxDB.
|
||||||
|
###
|
||||||
|
|
||||||
|
[tls]
|
||||||
|
# Determines the available set of cipher suites. See https://golang.org/pkg/crypto/tls/#pkg-constants
|
||||||
|
# for a list of available ciphers, which depends on the version of Go (use the query
|
||||||
|
# SHOW DIAGNOSTICS to see the version of Go used to build InfluxDB). If not specified, uses
|
||||||
|
# the default settings from Go's crypto/tls package.
|
||||||
|
# ciphers = [
|
||||||
|
# "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305",
|
||||||
|
# "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
|
||||||
|
# ]
|
||||||
|
|
||||||
|
# Minimum version of the tls protocol that will be negotiated. If not specified, uses the
|
||||||
|
# default settings from Go's crypto/tls package.
|
||||||
|
# min-version = "tls1.2"
|
||||||
|
|
||||||
|
# Maximum version of the tls protocol that will be negotiated. If not specified, uses the
|
||||||
|
# default settings from Go's crypto/tls package.
|
||||||
|
# max-version = "tls1.2"
|
||||||
4
build/extraResources/influxdb-1.7.0/start-influxdb.bat
Normal file
4
build/extraResources/influxdb-1.7.0/start-influxdb.bat
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
@echo off
|
||||||
|
cd /d "%~dp0"
|
||||||
|
influxd.exe -config "%~dp0influxdb.conf"
|
||||||
|
pause
|
||||||
@@ -10,9 +10,9 @@ export const getSteadyTrendIndicatorTree = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const querySteadyTrend = (params: SteadyDataView.SteadyTrendQueryParams) => {
|
export const querySteadyTrend = (params: SteadyDataView.SteadyTrendQueryParams) => {
|
||||||
return http.post<SteadyDataView.SteadyTrendQueryResult>('/steady/data-view/trend/query', params)
|
return http.post<SteadyDataView.SteadyTrendQueryResult>('/steady/data-view/trend/query', params, { loading: false })
|
||||||
}
|
}
|
||||||
|
|
||||||
export const querySteadyTrendDay = (params: SteadyDataView.SteadyTrendQueryParams) => {
|
export const querySteadyTrendDay = (params: SteadyDataView.SteadyTrendQueryParams) => {
|
||||||
return http.post<SteadyDataView.SteadyTrendQueryResult>('/steady/data-view/trend/day', params)
|
return http.post<SteadyDataView.SteadyTrendQueryResult>('/steady/data-view/trend/day', params, { loading: false })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ type AddDataRequestMethod = 'get' | 'post'
|
|||||||
|
|
||||||
const ADD_DATA_ROUTE_PATHS = ['/addData', '/api/addData'] as const
|
const ADD_DATA_ROUTE_PATHS = ['/addData', '/api/addData'] as const
|
||||||
const ADD_DATA_BASE_URL = String(import.meta.env.VITE_API_URL || '').trim()
|
const ADD_DATA_BASE_URL = String(import.meta.env.VITE_API_URL || '').trim()
|
||||||
|
const ADD_DATA_INFLUX_STORAGE_TYPE = 'INFLUXDB'
|
||||||
|
|
||||||
const resolveDevProxyTarget = () => {
|
const resolveDevProxyTarget = () => {
|
||||||
const proxyConfig = import.meta.env.VITE_PROXY
|
const proxyConfig = import.meta.env.VITE_PROXY
|
||||||
@@ -81,16 +82,25 @@ const requestAddData = async <T>(
|
|||||||
throw lastError
|
throw lastError
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const resolveTaskPathPrefix = (storageType?: AddData.StorageType) => {
|
||||||
|
// MySQL 与 InfluxDB 任务接口相互独立,创建和状态轮询必须使用同一入库类型前缀。
|
||||||
|
return storageType === ADD_DATA_INFLUX_STORAGE_TYPE ? '/influx/task' : '/task'
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getAddDataStorageTypeList = () => {
|
||||||
|
return requestAddData<AddData.StorageTypeItem[]>('get', '/storage-type/list')
|
||||||
|
}
|
||||||
|
|
||||||
export const getAddDataPreview = (params: AddData.TaskRequestParams) => {
|
export const getAddDataPreview = (params: AddData.TaskRequestParams) => {
|
||||||
return requestAddData<AddData.PreviewResponse>('post', '/task/preview', params)
|
return requestAddData<AddData.PreviewResponse>('post', '/task/preview', params)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createAddDataTask = (params: AddData.TaskRequestParams) => {
|
export const createAddDataTask = (params: AddData.TaskRequestParams, storageType?: AddData.StorageType) => {
|
||||||
return requestAddData<AddData.CreateTaskResponse>('post', '/task/create', params)
|
return requestAddData<AddData.CreateTaskResponse>('post', `${resolveTaskPathPrefix(storageType)}/create`, params)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getAddDataTaskStatus = (taskId: string | number) => {
|
export const getAddDataTaskStatus = (taskId: string | number, storageType?: AddData.StorageType) => {
|
||||||
return requestAddData<AddData.TaskStatusResponse>('get', `/task/status/${taskId}`)
|
return requestAddData<AddData.TaskStatusResponse>('get', `${resolveTaskPathPrefix(storageType)}/status/${taskId}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getAddDataTemplateList = () => {
|
export const getAddDataTemplateList = () => {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
export namespace AddData {
|
export namespace AddData {
|
||||||
export type LineMode = 'single' | 'multiple'
|
export type LineMode = 'single' | 'multiple'
|
||||||
export type IntervalMinutes = 1 | 3 | 5 | 10
|
export type IntervalMinutes = 1 | 3 | 5 | 10
|
||||||
|
export type StorageType = 'MYSQL' | 'INFLUXDB' | (string & {})
|
||||||
export type TaskStatus = 'WAITING' | 'RUNNING' | 'SUCCESS' | 'FAILED' | (string & {})
|
export type TaskStatus = 'WAITING' | 'RUNNING' | 'SUCCESS' | 'FAILED' | (string & {})
|
||||||
|
|
||||||
export interface TaskRequestParams {
|
export interface TaskRequestParams {
|
||||||
@@ -12,12 +13,18 @@ export namespace AddData {
|
|||||||
|
|
||||||
export interface TaskFormModel {
|
export interface TaskFormModel {
|
||||||
lineMode: LineMode
|
lineMode: LineMode
|
||||||
|
storageType: StorageType
|
||||||
lineIds: string[]
|
lineIds: string[]
|
||||||
startTime: string
|
startTime: string
|
||||||
endTime: string
|
endTime: string
|
||||||
intervalMinutes: IntervalMinutes
|
intervalMinutes: IntervalMinutes
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface StorageTypeItem {
|
||||||
|
code?: StorageType
|
||||||
|
name?: string
|
||||||
|
}
|
||||||
|
|
||||||
export interface PreviewTableStat {
|
export interface PreviewTableStat {
|
||||||
tableName?: string
|
tableName?: string
|
||||||
timePointCount?: number | string
|
timePointCount?: number | string
|
||||||
@@ -106,4 +113,9 @@ export namespace AddData {
|
|||||||
cp95ValueRule: string
|
cp95ValueRule: string
|
||||||
decimalScaleText: string
|
decimalScaleText: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface StorageTypeOption {
|
||||||
|
code: StorageType
|
||||||
|
name: string
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,12 +11,23 @@ const apiSource = fs.readFileSync(apiFile, 'utf8')
|
|||||||
const interfaceSource = fs.readFileSync(interfaceFile, 'utf8')
|
const interfaceSource = fs.readFileSync(interfaceFile, 'utf8')
|
||||||
|
|
||||||
const expectations = [
|
const expectations = [
|
||||||
|
[
|
||||||
|
'equipment payload uses network param as single source for mac and ndid',
|
||||||
|
/const networkParam = resolveOptionalText\(params\.mac\)/.test(apiSource) &&
|
||||||
|
/ndid:\s*networkParam/.test(apiSource) &&
|
||||||
|
/mac:\s*networkParam/.test(apiSource)
|
||||||
|
],
|
||||||
['equipment payload maps devType', /devType:\s*params\.dev_type/],
|
['equipment payload maps devType', /devType:\s*params\.dev_type/],
|
||||||
['equipment payload maps devModel', /devModel:\s*params\.dev_model/],
|
['equipment payload maps devModel', /devModel:\s*params\.dev_model/],
|
||||||
['equipment payload maps devAccessMethod', /devAccessMethod:\s*params\.dev_access_method/],
|
['equipment payload maps devAccessMethod', /devAccessMethod:\s*params\.dev_access_method/],
|
||||||
['equipment payload maps nodeId', /nodeId:\s*params\.node_id/],
|
['equipment payload maps nodeId', /nodeId:\s*params\.node_id/],
|
||||||
['equipment payload maps nodeProcess', /nodeProcess:\s*resolveOptionalNumber\(params\.node_process\)/],
|
['equipment payload maps nodeProcess', /nodeProcess:\s*resolveOptionalNumber\(params\.node_process\)/],
|
||||||
['line payload maps lineId', /lineId:\s*resolveOptionalText\(params\.line_id\s*\|\|\s*params\.id\)/],
|
['line payload resolves lineId only from existing line', /const lineId = resolveOptionalText\(params\.id\s*\|\|\s*params\.line_id\)/],
|
||||||
|
['line payload omits empty lineId field', /if \(lineId\) \{\s*payload\.lineId = lineId\s*\}/],
|
||||||
|
[
|
||||||
|
'new line form does not generate lineId before backend save',
|
||||||
|
/line_id:\s*''/.test(fs.readFileSync(path.join(currentDir, '..', '..', '..', 'views', 'tools', 'addLedger', 'utils', 'ledgerData.ts'), 'utf8'))
|
||||||
|
],
|
||||||
['line payload maps lineNo', /lineNo:\s*params\.line_no/],
|
['line payload maps lineNo', /lineNo:\s*params\.line_no/],
|
||||||
['line payload maps volGrade', /volGrade:\s*params\.vol_grade/],
|
['line payload maps volGrade', /volGrade:\s*params\.vol_grade/],
|
||||||
['line payload maps ctRatio', /ctRatio:\s*params\.ct_ratio/],
|
['line payload maps ctRatio', /ctRatio:\s*params\.ct_ratio/],
|
||||||
@@ -26,7 +37,10 @@ const expectations = [
|
|||||||
['delete response type is boolean', /requestAddLedger<boolean>\('delete',\s*'\/node'/]
|
['delete response type is boolean', /requestAddLedger<boolean>\('delete',\s*'\/node'/]
|
||||||
]
|
]
|
||||||
|
|
||||||
const failures = expectations.filter(([, pattern]) => !pattern.test(`${apiSource}\n${interfaceSource}`))
|
const source = `${apiSource}\n${interfaceSource}`
|
||||||
|
const failures = expectations.filter(([, expectation]) =>
|
||||||
|
typeof expectation === 'boolean' ? !expectation : !expectation.test(source)
|
||||||
|
)
|
||||||
|
|
||||||
if (failures.length) {
|
if (failures.length) {
|
||||||
console.error('addLedger API_DEBUG contract check failed:')
|
console.error('addLedger API_DEBUG contract check failed:')
|
||||||
|
|||||||
@@ -27,41 +27,54 @@ const toAddLedgerProjectPayload = (params: AddLedger.ProjectForm) => ({
|
|||||||
description: params.description
|
description: params.description
|
||||||
})
|
})
|
||||||
|
|
||||||
const toAddLedgerEquipmentPayload = (params: AddLedger.EquipmentForm) => ({
|
const toAddLedgerEquipmentPayload = (params: AddLedger.EquipmentForm) => {
|
||||||
id: resolveOptionalText(params.id),
|
// 后端仍接收 mac/ndid 两个字段,前端统一用“装置网络参数”作为唯一来源。
|
||||||
projectId: resolveOptionalText(params.projectId || params.parentId),
|
const networkParam = resolveOptionalText(params.mac)
|
||||||
name: params.name,
|
|
||||||
ndid: params.ndid,
|
|
||||||
mac: params.mac,
|
|
||||||
devType: params.dev_type,
|
|
||||||
devModel: params.dev_model,
|
|
||||||
devAccessMethod: params.dev_access_method,
|
|
||||||
nodeId: params.node_id,
|
|
||||||
nodeProcess: resolveOptionalNumber(params.node_process),
|
|
||||||
upgrade: params.upgrade
|
|
||||||
})
|
|
||||||
|
|
||||||
const toAddLedgerLinePayload = (params: AddLedger.LineForm) => ({
|
return {
|
||||||
lineId: resolveOptionalText(params.line_id || params.id),
|
id: resolveOptionalText(params.id),
|
||||||
deviceId: resolveOptionalText(params.deviceId || params.parentId),
|
projectId: resolveOptionalText(params.projectId || params.parentId),
|
||||||
name: params.name,
|
name: params.name,
|
||||||
lineNo: params.line_no,
|
ndid: networkParam,
|
||||||
conType: params.conType,
|
mac: networkParam,
|
||||||
volGrade: params.vol_grade,
|
devType: params.dev_type,
|
||||||
position: params.position,
|
devModel: params.dev_model,
|
||||||
ctRatio: params.ct_ratio,
|
devAccessMethod: params.dev_access_method,
|
||||||
ct2Ratio: params.ct2_ratio,
|
nodeId: params.node_id,
|
||||||
ptRatio: params.pt_ratio,
|
nodeProcess: resolveOptionalNumber(params.node_process),
|
||||||
pt2Ratio: params.pt2_ratio,
|
upgrade: params.upgrade
|
||||||
shortCircuitCapacity: params.short_circuit_capacity,
|
}
|
||||||
basicCapacity: params.basic_capacity,
|
}
|
||||||
protocolCapacity: params.protocol_capacity,
|
|
||||||
devCapacity: params.dev_capacity,
|
const toAddLedgerLinePayload = (params: AddLedger.LineForm) => {
|
||||||
monitorObj: params.monitor_obj,
|
const lineId = resolveOptionalText(params.id || params.line_id)
|
||||||
isGovern: params.is_govern,
|
const payload = {
|
||||||
monitorUser: params.monitor_user,
|
deviceId: resolveOptionalText(params.deviceId || params.parentId),
|
||||||
isImportant: params.is_important
|
name: params.name,
|
||||||
})
|
lineNo: params.line_no,
|
||||||
|
conType: params.conType,
|
||||||
|
volGrade: params.vol_grade,
|
||||||
|
position: params.position,
|
||||||
|
ctRatio: params.ct_ratio,
|
||||||
|
ct2Ratio: params.ct2_ratio,
|
||||||
|
ptRatio: params.pt_ratio,
|
||||||
|
pt2Ratio: params.pt2_ratio,
|
||||||
|
shortCircuitCapacity: params.short_circuit_capacity,
|
||||||
|
basicCapacity: params.basic_capacity,
|
||||||
|
protocolCapacity: params.protocol_capacity,
|
||||||
|
devCapacity: params.dev_capacity,
|
||||||
|
monitorObj: params.monitor_obj,
|
||||||
|
isGovern: params.is_govern,
|
||||||
|
monitorUser: params.monitor_user,
|
||||||
|
isImportant: params.is_important
|
||||||
|
} as Record<string, unknown>
|
||||||
|
|
||||||
|
if (lineId) {
|
||||||
|
payload.lineId = lineId
|
||||||
|
}
|
||||||
|
|
||||||
|
return payload
|
||||||
|
}
|
||||||
|
|
||||||
const resolveDevProxyTarget = () => {
|
const resolveDevProxyTarget = () => {
|
||||||
const proxyConfig = import.meta.env.VITE_PROXY
|
const proxyConfig = import.meta.env.VITE_PROXY
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ export const DICT_CODES = {
|
|||||||
USER_STATE: 'state',
|
USER_STATE: 'state',
|
||||||
EVENT_TYPE: 'event_type',
|
EVENT_TYPE: 'event_type',
|
||||||
LEDGER_DEVICE_TYPE: 'ledger_device_type',
|
LEDGER_DEVICE_TYPE: 'ledger_device_type',
|
||||||
LEDGER_DEVICE_MODEL: 'Ex-factory_Dev_Type'
|
LEDGER_DEVICE_MODEL: 'Ex-factory_Dev_Type',
|
||||||
|
LEDGER_TERMINAL_MODEL: 'Dev_Type'
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
export type DictCode = (typeof DICT_CODES)[keyof typeof DICT_CODES]
|
export type DictCode = (typeof DICT_CODES)[keyof typeof DICT_CODES]
|
||||||
|
|||||||
@@ -6,14 +6,6 @@
|
|||||||
circle
|
circle
|
||||||
@click="emit('update:collapsed', !collapsed)"
|
@click="emit('update:collapsed', !collapsed)"
|
||||||
/>
|
/>
|
||||||
<button
|
|
||||||
v-if="collapsed"
|
|
||||||
class="indicator-collapsed-trigger"
|
|
||||||
type="button"
|
|
||||||
@click="emit('update:collapsed', false)"
|
|
||||||
>
|
|
||||||
{{ collapsedLabel }}
|
|
||||||
</button>
|
|
||||||
<div v-show="!collapsed" class="indicator-panel-body">
|
<div v-show="!collapsed" class="indicator-panel-body">
|
||||||
<SteadyIndicatorTree
|
<SteadyIndicatorTree
|
||||||
:key="selectorResetKey"
|
:key="selectorResetKey"
|
||||||
@@ -49,8 +41,6 @@ const emit = defineEmits<{
|
|||||||
refresh: []
|
refresh: []
|
||||||
change: [nodes: SteadyDataView.SteadyIndicatorNode[]]
|
change: [nodes: SteadyDataView.SteadyIndicatorNode[]]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const collapsedLabel = '\u7a33\u6001\u6307\u6807'
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
@@ -60,12 +50,12 @@ const collapsedLabel = '\u7a33\u6001\u6307\u6807'
|
|||||||
right: 12px;
|
right: 12px;
|
||||||
bottom: 12px;
|
bottom: 12px;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
width: 360px;
|
width: 300px;
|
||||||
transition: width 0.2s ease;
|
transition: width 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.indicator-floating-panel.is-collapsed {
|
.indicator-floating-panel.is-collapsed {
|
||||||
width: 44px;
|
width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.indicator-toggle {
|
.indicator-toggle {
|
||||||
@@ -85,33 +75,13 @@ const collapsedLabel = '\u7a33\u6001\u6307\u6807'
|
|||||||
box-shadow: var(--el-box-shadow-light);
|
box-shadow: var(--el-box-shadow-light);
|
||||||
}
|
}
|
||||||
|
|
||||||
.indicator-collapsed-trigger {
|
.indicator-floating-panel.is-collapsed .indicator-panel-body {
|
||||||
display: flex;
|
display: none;
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
padding: 8px 0;
|
|
||||||
border: 1px solid var(--el-border-color-light);
|
|
||||||
border-radius: 4px;
|
|
||||||
background: var(--el-bg-color);
|
|
||||||
box-shadow: var(--el-box-shadow-light);
|
|
||||||
color: var(--el-text-color-primary);
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 600;
|
|
||||||
line-height: 1.2;
|
|
||||||
writing-mode: vertical-rl;
|
|
||||||
}
|
|
||||||
|
|
||||||
.indicator-collapsed-trigger:hover {
|
|
||||||
border-color: var(--el-color-primary-light-5);
|
|
||||||
color: var(--el-color-primary);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 1360px) {
|
@media (max-width: 1360px) {
|
||||||
.indicator-floating-panel {
|
.indicator-floating-panel {
|
||||||
width: 320px;
|
width: 280px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -96,7 +96,6 @@ watch(
|
|||||||
padding: 12px;
|
padding: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.panel-header,
|
|
||||||
.tree-node {
|
.tree-node {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -104,6 +103,13 @@ watch(
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.panel-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
.panel-title {
|
.panel-title {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
|||||||
@@ -34,8 +34,8 @@
|
|||||||
<span class="node-name">{{ data.name }}</span>
|
<span class="node-name">{{ data.name }}</span>
|
||||||
</span>
|
</span>
|
||||||
<span class="node-count">
|
<span class="node-count">
|
||||||
<template v-if="Number(data.deviceCount) || Number(data.lineCount)">
|
<template v-if="shouldShowLedgerCount(data)">
|
||||||
{{ Number(data.deviceCount || 0) }} / {{ Number(data.lineCount || 0) }}
|
{{ resolveLedgerCountText(data) }}
|
||||||
</template>
|
</template>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -91,6 +91,18 @@ const resolveLedgerIcon = (value: unknown) => {
|
|||||||
return ledgerIcons[normalizeLedgerLevel(value)]
|
return ledgerIcons[normalizeLedgerLevel(value)]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const shouldShowLedgerCount = (data: SteadyDataView.SteadyLedgerNode) => {
|
||||||
|
return Number(data.level) < 3 && (Number(data.deviceCount) > 0 || Number(data.lineCount) > 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolveLedgerCountText = (data: SteadyDataView.SteadyLedgerNode) => {
|
||||||
|
if (normalizeLedgerLevel(data.level) === 2) {
|
||||||
|
return String(Number(data.lineCount || 0))
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${Number(data.deviceCount || 0)} / ${Number(data.lineCount || 0)}`
|
||||||
|
}
|
||||||
|
|
||||||
const handleKeywordChange = (value: string) => {
|
const handleKeywordChange = (value: string) => {
|
||||||
emit('search', value)
|
emit('search', value)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<section class="card trend-chart-panel" v-loading="loading">
|
<section class="card trend-chart-panel" v-loading="loading">
|
||||||
<div class="panel-header">
|
<div v-if="trendResult" class="panel-header">
|
||||||
<span class="panel-title">趋势图</span>
|
|
||||||
<span class="panel-meta">
|
<span class="panel-meta">
|
||||||
<template v-if="trendResult">
|
{{ trendResult.bucket || '-' }} / {{ trendResult.displayPointCount || 0 }} 点
|
||||||
{{ trendResult.bucket || '-' }} / {{ trendResult.displayPointCount || 0 }} 点
|
|
||||||
</template>
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -41,6 +38,7 @@ const chartOptions = computed(() => buildSteadyTrendChartOptions(props.trendResu
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,16 +46,11 @@ const chartOptions = computed(() => buildSteadyTrendChartOptions(props.trendResu
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex: none;
|
flex: none;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: flex-end;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.panel-title {
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel-meta {
|
.panel-meta {
|
||||||
color: var(--el-text-color-secondary);
|
color: var(--el-text-color-secondary);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ const handleTimeBaseDateChange = (value: Date) => {
|
|||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.trend-toolbar {
|
.trend-toolbar {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(312px, 1.4fr) repeat(2, minmax(178px, 0.8fr)) auto;
|
grid-template-columns: repeat(4, minmax(0, 1fr)) auto;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
@@ -132,7 +132,7 @@ const handleTimeBaseDateChange = (value: Date) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.toolbar-field--time {
|
.toolbar-field--time {
|
||||||
min-width: 312px;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toolbar-field__label {
|
.toolbar-field__label {
|
||||||
@@ -149,15 +149,16 @@ const handleTimeBaseDateChange = (value: Date) => {
|
|||||||
|
|
||||||
.trend-toolbar__time {
|
.trend-toolbar__time {
|
||||||
flex: 1 1 0;
|
flex: 1 1 0;
|
||||||
min-width: 260px;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.harmonic-select {
|
.harmonic-select {
|
||||||
grid-column: span 2;
|
grid-column: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toolbar-actions {
|
.toolbar-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
grid-column: 5;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -107,6 +107,7 @@ const indicatorPanelCollapsedProxy = computed({
|
|||||||
.selector-column {
|
.selector-column {
|
||||||
display: grid;
|
display: grid;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.trend-main {
|
.trend-main {
|
||||||
@@ -115,12 +116,14 @@ const indicatorPanelCollapsedProxy = computed({
|
|||||||
gap: 12px;
|
gap: 12px;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.trend-content {
|
.trend-content {
|
||||||
position: relative;
|
position: relative;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.trend-content :deep(.trend-chart-panel) {
|
.trend-content :deep(.trend-chart-panel) {
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
/* eslint-env node */
|
||||||
|
import fs from 'node:fs'
|
||||||
|
import { createRequire } from 'node:module'
|
||||||
|
import path from 'node:path'
|
||||||
|
import vm from 'node:vm'
|
||||||
|
import { fileURLToPath } from 'node:url'
|
||||||
|
import ts from 'typescript'
|
||||||
|
|
||||||
|
const currentDir = path.dirname(fileURLToPath(import.meta.url))
|
||||||
|
const require = createRequire(import.meta.url)
|
||||||
|
const utilsFile = path.join(currentDir, '..', 'utils', 'ledgerTree.ts')
|
||||||
|
const pageFile = path.join(currentDir, '..', 'index.vue')
|
||||||
|
const ledgerTreeComponentFile = path.join(currentDir, '..', 'components', 'SteadyLedgerTree.vue')
|
||||||
|
|
||||||
|
const read = file => fs.readFileSync(file, 'utf8')
|
||||||
|
|
||||||
|
if (!fs.existsSync(utilsFile)) {
|
||||||
|
console.error('steadyDataView ledger tree normalize contract failed:')
|
||||||
|
console.error('- ledgerTree utility should exist')
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const source = read(utilsFile)
|
||||||
|
const compiled = ts.transpileModule(source, {
|
||||||
|
compilerOptions: {
|
||||||
|
module: ts.ModuleKind.CommonJS,
|
||||||
|
target: ts.ScriptTarget.ES2020,
|
||||||
|
esModuleInterop: true
|
||||||
|
}
|
||||||
|
}).outputText
|
||||||
|
|
||||||
|
const moduleContext = { exports: {} }
|
||||||
|
const sandbox = {
|
||||||
|
exports: moduleContext.exports,
|
||||||
|
module: moduleContext,
|
||||||
|
require
|
||||||
|
}
|
||||||
|
|
||||||
|
vm.runInNewContext(compiled, sandbox, { filename: utilsFile })
|
||||||
|
|
||||||
|
const { normalizeSteadyLedgerTree } = moduleContext.exports
|
||||||
|
|
||||||
|
if (typeof normalizeSteadyLedgerTree !== 'function') {
|
||||||
|
console.error('steadyDataView ledger tree normalize contract failed:')
|
||||||
|
console.error('- normalizeSteadyLedgerTree should be exported')
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const flatNodes = [
|
||||||
|
{ id: 'line-1', parentId: 'device-1', name: '监测点_1', level: 3, lineCount: 1, selectable: true },
|
||||||
|
{ id: 'line-disabled', parentId: 'device-1', name: '不可选监测点', level: 3, lineCount: 1, selectable: false },
|
||||||
|
{ id: 'engineering-1', name: '工程', level: 0, deviceCount: 1, lineCount: 2 },
|
||||||
|
{ id: 'device-1', parentId: 'project-1', name: '设备', level: 2, lineCount: 2 },
|
||||||
|
{ id: 'project-1', parentId: 'engineering-1', name: '项目', level: 1, deviceCount: 1, lineCount: 2 },
|
||||||
|
{ id: 'line-2', parentId: 'device-1', name: '监测点_2', level: 3, lineCount: 1, selectable: true }
|
||||||
|
]
|
||||||
|
|
||||||
|
const normalized = normalizeSteadyLedgerTree(flatNodes)
|
||||||
|
const expectedPath = normalized[0]?.children?.[0]?.children?.[0]?.children?.map(item => item.name)
|
||||||
|
|
||||||
|
const expectations = [
|
||||||
|
['flat nodes rebuild to one root engineering node', normalized.length === 1 && normalized[0].id === 'engineering-1'],
|
||||||
|
['project is nested under engineering', normalized[0]?.children?.[0]?.id === 'project-1'],
|
||||||
|
['device is nested under project', normalized[0]?.children?.[0]?.children?.[0]?.id === 'device-1'],
|
||||||
|
['lines are nested under device', JSON.stringify(expectedPath) === JSON.stringify(['监测点_1', '监测点_2'])],
|
||||||
|
[
|
||||||
|
'line nodes keep selectable query identity',
|
||||||
|
normalized[0]?.children?.[0]?.children?.[0]?.children?.every(item => item.selectable && item.level === 3)
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'unselectable line nodes are removed from steady query tree',
|
||||||
|
!normalized[0]?.children?.[0]?.children?.[0]?.children?.some(item => item.id === 'line-disabled')
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'ledger tree component hides count text on monitor point leaves',
|
||||||
|
/shouldShowLedgerCount[\s\S]*Number\(data\.level\)\s*<\s*3/.test(read(ledgerTreeComponentFile))
|
||||||
|
],
|
||||||
|
['page uses normalized ledger tree data', /normalizeSteadyLedgerTree/.test(read(pageFile))]
|
||||||
|
]
|
||||||
|
|
||||||
|
const failures = expectations.filter(([, matched]) => !matched)
|
||||||
|
|
||||||
|
if (failures.length) {
|
||||||
|
console.error('steadyDataView ledger tree normalize contract failed:')
|
||||||
|
for (const [name] of failures) {
|
||||||
|
console.error(`- ${name}`)
|
||||||
|
}
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('steadyDataView ledger tree normalize contract passed')
|
||||||
@@ -45,6 +45,8 @@ const expectations = [
|
|||||||
['API exposes indicator tree endpoint', /\/steady\/data-view\/indicator-tree/],
|
['API exposes indicator tree endpoint', /\/steady\/data-view\/indicator-tree/],
|
||||||
['API exposes trend query endpoint', /\/steady\/data-view\/trend\/query/],
|
['API exposes trend query endpoint', /\/steady\/data-view\/trend\/query/],
|
||||||
['API exposes trend day endpoint', /\/steady\/data-view\/trend\/day/],
|
['API exposes trend day endpoint', /\/steady\/data-view\/trend\/day/],
|
||||||
|
['API disables global loading for trend query', /querySteadyTrend[\s\S]*\/steady\/data-view\/trend\/query[\s\S]*loading:\s*false/],
|
||||||
|
['API disables global loading for trend day query', /querySteadyTrendDay[\s\S]*\/steady\/data-view\/trend\/day[\s\S]*loading:\s*false/],
|
||||||
['API does not expose trend summary endpoint', /\/steady\/data-view\/trend\/summary/],
|
['API does not expose trend summary endpoint', /\/steady\/data-view\/trend\/summary/],
|
||||||
['interfaces define trend query params', /interface\s+SteadyTrendQueryParams/],
|
['interfaces define trend query params', /interface\s+SteadyTrendQueryParams/],
|
||||||
['interfaces define trend series', /interface\s+SteadyTrendSeries/],
|
['interfaces define trend series', /interface\s+SteadyTrendSeries/],
|
||||||
@@ -89,6 +91,8 @@ const sourceByExpectation = [
|
|||||||
apiSource,
|
apiSource,
|
||||||
apiSource,
|
apiSource,
|
||||||
apiSource,
|
apiSource,
|
||||||
|
apiSource,
|
||||||
|
apiSource,
|
||||||
interfaceSource,
|
interfaceSource,
|
||||||
interfaceSource,
|
interfaceSource,
|
||||||
interfaceSource,
|
interfaceSource,
|
||||||
|
|||||||
@@ -17,6 +17,11 @@ const componentSource = fs.existsSync(componentDir)
|
|||||||
.map(file => fs.readFileSync(path.join(componentDir, file), 'utf8'))
|
.map(file => fs.readFileSync(path.join(componentDir, file), 'utf8'))
|
||||||
.join('\n')
|
.join('\n')
|
||||||
: ''
|
: ''
|
||||||
|
const readComponent = file => fs.readFileSync(path.join(componentDir, file), 'utf8')
|
||||||
|
const toolbarSource = readComponent('SteadyTrendToolbar.vue')
|
||||||
|
const chartPanelSource = readComponent('SteadyTrendChartPanel.vue')
|
||||||
|
const floatingPanelSource = readComponent('SteadyIndicatorFloatingPanel.vue')
|
||||||
|
const indicatorTreeSource = readComponent('SteadyIndicatorTree.vue')
|
||||||
const viewSource = `${source}\n${componentSource}`
|
const viewSource = `${source}\n${componentSource}`
|
||||||
const apiSource = fs.readFileSync(apiFile, 'utf8')
|
const apiSource = fs.readFileSync(apiFile, 'utf8')
|
||||||
const interfaceSource = fs.readFileSync(interfaceFile, 'utf8')
|
const interfaceSource = fs.readFileSync(interfaceFile, 'utf8')
|
||||||
@@ -40,7 +45,10 @@ const forbiddenPatterns = [
|
|||||||
'trend summary types are removed',
|
'trend summary types are removed',
|
||||||
/SteadyTrendSummary|SteadyTrendSummaryItem/,
|
/SteadyTrendSummary|SteadyTrendSummaryItem/,
|
||||||
interfaceSource
|
interfaceSource
|
||||||
]
|
],
|
||||||
|
['chart panel title text is removed', /panel-title/, chartPanelSource],
|
||||||
|
['collapsed indicator vertical trigger is removed', /indicator-collapsed-trigger/, floatingPanelSource],
|
||||||
|
['collapsed indicator label is removed', /collapsedLabel/, floatingPanelSource]
|
||||||
]
|
]
|
||||||
|
|
||||||
const requiredPatterns = [
|
const requiredPatterns = [
|
||||||
@@ -52,7 +60,17 @@ const requiredPatterns = [
|
|||||||
['components keep right floating indicator panel', /indicator-floating-panel/, viewSource],
|
['components keep right floating indicator panel', /indicator-floating-panel/, viewSource],
|
||||||
['indicator panel defaults expanded', /indicatorPanelCollapsed\s*=\s*ref\(false\)/, source],
|
['indicator panel defaults expanded', /indicatorPanelCollapsed\s*=\s*ref\(false\)/, source],
|
||||||
['indicator panel supports collapsed state', /is-collapsed/, viewSource],
|
['indicator panel supports collapsed state', /is-collapsed/, viewSource],
|
||||||
['API keeps trend query endpoint', /\/steady\/data-view\/trend\/query/, apiSource]
|
['API keeps trend query endpoint', /\/steady\/data-view\/trend\/query/, apiSource],
|
||||||
|
[
|
||||||
|
'trend toolbar reserves four evenly distributed search columns',
|
||||||
|
/grid-template-columns:\s*repeat\(4,\s*minmax\(0,\s*1fr\)\)\s*auto/,
|
||||||
|
toolbarSource
|
||||||
|
],
|
||||||
|
['trend toolbar keeps actions after four search columns', /grid-column:\s*5/, toolbarSource],
|
||||||
|
['floating indicator panel expanded width is reduced', /width:\s*300px/, floatingPanelSource],
|
||||||
|
['floating indicator collapsed state keeps icon only', /width:\s*0/, floatingPanelSource],
|
||||||
|
['floating indicator body is hidden when collapsed', /\.indicator-floating-panel\.is-collapsed\s+\.indicator-panel-body/, floatingPanelSource],
|
||||||
|
['indicator tree header separates title and refresh icon', /justify-content:\s*flex-start/, indicatorTreeSource]
|
||||||
]
|
]
|
||||||
|
|
||||||
const failures = [
|
const failures = [
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ import {
|
|||||||
resolveAvailableStats,
|
resolveAvailableStats,
|
||||||
validateTrendSelection
|
validateTrendSelection
|
||||||
} from './utils/selectionRules'
|
} from './utils/selectionRules'
|
||||||
|
import { normalizeSteadyLedgerTree } from './utils/ledgerTree'
|
||||||
import { buildSteadyTrendQueryPayload, defaultTrendFormState } from './utils/trendPayload'
|
import { buildSteadyTrendQueryPayload, defaultTrendFormState } from './utils/trendPayload'
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
@@ -80,7 +81,8 @@ const loadLedgerTree = async (keyword = ledgerKeyword.value) => {
|
|||||||
loading.ledger = true
|
loading.ledger = true
|
||||||
try {
|
try {
|
||||||
const response = await getSteadyTrendLedgerTree(keyword ? { keyword } : undefined)
|
const response = await getSteadyTrendLedgerTree(keyword ? { keyword } : undefined)
|
||||||
ledgerTree.value = unwrapData(response) || []
|
// 台账树接口在搜索场景可能返回扁平节点,前端统一恢复工程、项目、设备、监测点层级。
|
||||||
|
ledgerTree.value = normalizeSteadyLedgerTree(unwrapData(response) || [])
|
||||||
const firstLedgerNode = findFirstSelectableLedgerNode(ledgerTree.value)
|
const firstLedgerNode = findFirstSelectableLedgerNode(ledgerTree.value)
|
||||||
// 台账树首次加载后默认选中第一个可查询监测点,避免趋势查询初始状态为空。
|
// 台账树首次加载后默认选中第一个可查询监测点,避免趋势查询初始状态为空。
|
||||||
selectedLedgerNodes.value = firstLedgerNode ? [firstLedgerNode] : []
|
selectedLedgerNodes.value = firstLedgerNode ? [firstLedgerNode] : []
|
||||||
@@ -184,6 +186,7 @@ onMounted(() => {
|
|||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.steady-data-view-page {
|
.steady-data-view-page {
|
||||||
|
height: 100%;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|||||||
170
frontend/src/views/steady/steadyDataView/utils/ledgerTree.ts
Normal file
170
frontend/src/views/steady/steadyDataView/utils/ledgerTree.ts
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
import type { SteadyDataView } from '@/api/steady/steadyDataView/interface'
|
||||||
|
|
||||||
|
type LedgerLevel = SteadyDataView.SteadyLedgerNode['level']
|
||||||
|
type RawLedgerNode = Partial<SteadyDataView.SteadyLedgerNode> & Record<string, unknown>
|
||||||
|
type IndexedLedgerNode = SteadyDataView.SteadyLedgerNode & {
|
||||||
|
parentIds?: string
|
||||||
|
__order: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolveText = (data: RawLedgerNode, ...keys: string[]) => {
|
||||||
|
for (const key of keys) {
|
||||||
|
const value = data[key]
|
||||||
|
if (value === null || value === undefined) continue
|
||||||
|
const text = String(value).trim()
|
||||||
|
if (text) return text
|
||||||
|
}
|
||||||
|
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolveNumber = (data: RawLedgerNode, ...keys: string[]) => {
|
||||||
|
for (const key of keys) {
|
||||||
|
const value = data[key]
|
||||||
|
if (value === null || value === undefined || value === '') continue
|
||||||
|
const parsed = Number(value)
|
||||||
|
if (Number.isFinite(parsed)) return parsed
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolveBoolean = (data: RawLedgerNode, ...keys: string[]) => {
|
||||||
|
for (const key of keys) {
|
||||||
|
const value = data[key]
|
||||||
|
if (value === null || value === undefined || value === '') continue
|
||||||
|
if (typeof value === 'boolean') return value
|
||||||
|
if (typeof value === 'number') return value !== 0
|
||||||
|
|
||||||
|
const text = String(value).trim().toLowerCase()
|
||||||
|
if (text === 'true' || text === '1') return true
|
||||||
|
if (text === 'false' || text === '0') return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizeLevel = (value: unknown): LedgerLevel => {
|
||||||
|
const level = Number(value)
|
||||||
|
if (level === 0 || level === 1 || level === 2 || level === 3) {
|
||||||
|
return level
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
const splitParentIds = (parentIds?: string) => {
|
||||||
|
if (!parentIds) return []
|
||||||
|
|
||||||
|
return parentIds
|
||||||
|
.split(/[,\s/|>]+/)
|
||||||
|
.map(item => item.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
}
|
||||||
|
|
||||||
|
const flattenLedgerNodes = (
|
||||||
|
nodes: SteadyDataView.SteadyLedgerNode[],
|
||||||
|
output: IndexedLedgerNode[] = [],
|
||||||
|
inheritedParentId = ''
|
||||||
|
) => {
|
||||||
|
nodes.forEach(node => {
|
||||||
|
const rawNode = node as RawLedgerNode
|
||||||
|
const children = Array.isArray(node.children) ? node.children : []
|
||||||
|
const id = resolveText(rawNode, 'id', 'Id')
|
||||||
|
const parentId = resolveText(rawNode, 'parentId', 'pid', 'Pid') || inheritedParentId
|
||||||
|
const level = normalizeLevel(rawNode.level ?? rawNode.Level)
|
||||||
|
const rawSelectable = resolveBoolean(rawNode, 'selectable', 'Selectable')
|
||||||
|
|
||||||
|
if (!id) return
|
||||||
|
if (level === 3 && rawSelectable === false) return
|
||||||
|
|
||||||
|
output.push({
|
||||||
|
id,
|
||||||
|
parentId,
|
||||||
|
parentIds: resolveText(rawNode, 'parentIds', 'pids', 'Pids'),
|
||||||
|
name: resolveText(rawNode, 'name', 'Name') || id,
|
||||||
|
level,
|
||||||
|
sort: resolveNumber(rawNode, 'sort', 'Sort'),
|
||||||
|
deviceCount: resolveNumber(rawNode, 'deviceCount', 'DeviceCount'),
|
||||||
|
lineCount: resolveNumber(rawNode, 'lineCount', 'LineCount'),
|
||||||
|
selectable: level === 3 ? rawSelectable !== false : rawSelectable === true,
|
||||||
|
children: [],
|
||||||
|
__order: output.length
|
||||||
|
})
|
||||||
|
|
||||||
|
flattenLedgerNodes(children, output, id)
|
||||||
|
})
|
||||||
|
|
||||||
|
return output
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolveExpectedParentId = (node: IndexedLedgerNode, nodeMap: Map<string, IndexedLedgerNode>) => {
|
||||||
|
if (node.level === 0) return ''
|
||||||
|
|
||||||
|
const parentNode = node.parentId ? nodeMap.get(node.parentId) : undefined
|
||||||
|
if (parentNode && parentNode.level === node.level - 1) {
|
||||||
|
return parentNode.id
|
||||||
|
}
|
||||||
|
|
||||||
|
// 后端搜索可能返回扁平节点或错误嵌套,优先按 parentIds 中的上一层节点恢复固定台账层级。
|
||||||
|
const parentIds = splitParentIds(node.parentIds).reverse()
|
||||||
|
const matchedParent = parentIds.map(id => nodeMap.get(id)).find(item => item && item.level === node.level - 1)
|
||||||
|
|
||||||
|
return matchedParent?.id || node.parentId || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const sortLedgerNodes = (left: IndexedLedgerNode, right: IndexedLedgerNode) => {
|
||||||
|
const leftSort = Number.isFinite(left.sort) ? Number(left.sort) : left.__order
|
||||||
|
const rightSort = Number.isFinite(right.sort) ? Number(right.sort) : right.__order
|
||||||
|
if (leftSort !== rightSort) return leftSort - rightSort
|
||||||
|
|
||||||
|
return left.__order - right.__order
|
||||||
|
}
|
||||||
|
|
||||||
|
const stripInternalFields = (node: IndexedLedgerNode): SteadyDataView.SteadyLedgerNode => {
|
||||||
|
return {
|
||||||
|
id: node.id,
|
||||||
|
parentId: node.parentId,
|
||||||
|
name: node.name,
|
||||||
|
level: node.level,
|
||||||
|
sort: node.sort,
|
||||||
|
deviceCount: node.deviceCount,
|
||||||
|
lineCount: node.lineCount,
|
||||||
|
selectable: node.selectable,
|
||||||
|
children: node.children?.map(item => stripInternalFields(item as IndexedLedgerNode))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const normalizeSteadyLedgerTree = (
|
||||||
|
nodes: SteadyDataView.SteadyLedgerNode[] = []
|
||||||
|
): SteadyDataView.SteadyLedgerNode[] => {
|
||||||
|
const flatNodes = flattenLedgerNodes(nodes)
|
||||||
|
const nodeMap = new Map(flatNodes.map(node => [node.id, node]))
|
||||||
|
const roots: IndexedLedgerNode[] = []
|
||||||
|
|
||||||
|
flatNodes.forEach(node => {
|
||||||
|
node.children = []
|
||||||
|
})
|
||||||
|
|
||||||
|
flatNodes.forEach(node => {
|
||||||
|
const parentId = resolveExpectedParentId(node, nodeMap)
|
||||||
|
const parentNode = parentId ? nodeMap.get(parentId) : undefined
|
||||||
|
|
||||||
|
if (!parentNode || parentNode.id === node.id) {
|
||||||
|
roots.push(node)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
node.parentId = parentNode.id
|
||||||
|
parentNode.children = [...(parentNode.children || []), node]
|
||||||
|
})
|
||||||
|
|
||||||
|
const sortChildren = (items: IndexedLedgerNode[]) => {
|
||||||
|
items.sort(sortLedgerNodes)
|
||||||
|
items.forEach(item => sortChildren((item.children || []) as IndexedLedgerNode[]))
|
||||||
|
}
|
||||||
|
|
||||||
|
sortChildren(roots)
|
||||||
|
|
||||||
|
return roots.map(stripInternalFields)
|
||||||
|
}
|
||||||
@@ -3,11 +3,15 @@ import type { SteadyDataView } from '@/api/steady/steadyDataView/interface'
|
|||||||
export const MAX_TREND_SERIES_COUNT = 24
|
export const MAX_TREND_SERIES_COUNT = 24
|
||||||
export const MAX_HARMONIC_ORDER_COUNT = 6
|
export const MAX_HARMONIC_ORDER_COUNT = 6
|
||||||
|
|
||||||
|
const isSelectableLineNode = (node: SteadyDataView.SteadyLedgerNode) => {
|
||||||
|
return node.level === 3 && node.selectable !== false
|
||||||
|
}
|
||||||
|
|
||||||
export const collectSelectedLineIds = (nodes: SteadyDataView.SteadyLedgerNode[]) => {
|
export const collectSelectedLineIds = (nodes: SteadyDataView.SteadyLedgerNode[]) => {
|
||||||
const lineIds = new Set<string>()
|
const lineIds = new Set<string>()
|
||||||
|
|
||||||
const collect = (node: SteadyDataView.SteadyLedgerNode) => {
|
const collect = (node: SteadyDataView.SteadyLedgerNode) => {
|
||||||
if (node.level === 3 || node.selectable) {
|
if (isSelectableLineNode(node)) {
|
||||||
lineIds.add(node.id)
|
lineIds.add(node.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,7 +46,7 @@ export const findFirstSelectableLedgerNode = (
|
|||||||
nodes: SteadyDataView.SteadyLedgerNode[]
|
nodes: SteadyDataView.SteadyLedgerNode[]
|
||||||
): SteadyDataView.SteadyLedgerNode | null => {
|
): SteadyDataView.SteadyLedgerNode | null => {
|
||||||
for (const node of nodes) {
|
for (const node of nodes) {
|
||||||
if (node.level === 3 || node.selectable) {
|
if (isSelectableLineNode(node)) {
|
||||||
return node
|
return node
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,16 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<el-form ref="formRef" :model="localForm" :rules="formRules" label-width="108px" class="task-form">
|
<el-form ref="formRef" :model="localForm" :rules="formRules" label-width="108px" class="task-form">
|
||||||
|
<div class="form-row form-row-storage">
|
||||||
|
<el-form-item class="form-item-storage-type" label="入库类型" prop="storageType">
|
||||||
|
<el-radio-group v-model="localForm.storageType" :disabled="taskRunning || storageTypeLoading">
|
||||||
|
<el-radio-button v-for="item in storageTypeOptions" :key="item.code" :label="item.code">
|
||||||
|
{{ item.name }}
|
||||||
|
</el-radio-button>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-row form-row-first">
|
<div class="form-row form-row-first">
|
||||||
<el-form-item class="form-item-line-ids" label="监测点 ID" prop="lineIds">
|
<el-form-item class="form-item-line-ids" label="监测点 ID" prop="lineIds">
|
||||||
<div class="line-id-input-group">
|
<div class="line-id-input-group">
|
||||||
@@ -143,6 +153,8 @@ defineOptions({
|
|||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
form: AddData.TaskFormModel
|
form: AddData.TaskFormModel
|
||||||
preview: AddData.NormalizedPreview | null
|
preview: AddData.NormalizedPreview | null
|
||||||
|
storageTypeOptions: AddData.StorageTypeOption[]
|
||||||
|
storageTypeLoading: boolean
|
||||||
previewLoading: boolean
|
previewLoading: boolean
|
||||||
submitLoading: boolean
|
submitLoading: boolean
|
||||||
taskRunning: boolean
|
taskRunning: boolean
|
||||||
@@ -161,6 +173,7 @@ const guidCount = ref(1)
|
|||||||
const intervalOptions: AddData.IntervalMinutes[] = [1, 3, 5, 10]
|
const intervalOptions: AddData.IntervalMinutes[] = [1, 3, 5, 10]
|
||||||
const localForm = reactive<AddData.TaskFormModel>({
|
const localForm = reactive<AddData.TaskFormModel>({
|
||||||
lineMode: 'multiple',
|
lineMode: 'multiple',
|
||||||
|
storageType: props.form.storageType,
|
||||||
lineIds: [...props.form.lineIds],
|
lineIds: [...props.form.lineIds],
|
||||||
startTime: props.form.startTime,
|
startTime: props.form.startTime,
|
||||||
endTime: props.form.endTime,
|
endTime: props.form.endTime,
|
||||||
@@ -169,6 +182,7 @@ const localForm = reactive<AddData.TaskFormModel>({
|
|||||||
|
|
||||||
const syncLocalForm = (form: AddData.TaskFormModel) => {
|
const syncLocalForm = (form: AddData.TaskFormModel) => {
|
||||||
localForm.lineMode = 'multiple'
|
localForm.lineMode = 'multiple'
|
||||||
|
localForm.storageType = form.storageType
|
||||||
localForm.lineIds = [...form.lineIds]
|
localForm.lineIds = [...form.lineIds]
|
||||||
localForm.startTime = form.startTime
|
localForm.startTime = form.startTime
|
||||||
localForm.endTime = form.endTime
|
localForm.endTime = form.endTime
|
||||||
@@ -216,6 +230,7 @@ watch(
|
|||||||
|
|
||||||
emit('update:form', {
|
emit('update:form', {
|
||||||
lineMode: 'multiple',
|
lineMode: 'multiple',
|
||||||
|
storageType: value.storageType,
|
||||||
lineIds: [...value.lineIds],
|
lineIds: [...value.lineIds],
|
||||||
startTime: value.startTime,
|
startTime: value.startTime,
|
||||||
endTime: value.endTime,
|
endTime: value.endTime,
|
||||||
@@ -271,6 +286,7 @@ const handleAppendGuids = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const formRules: FormRules<AddData.TaskFormModel> = {
|
const formRules: FormRules<AddData.TaskFormModel> = {
|
||||||
|
storageType: [{ required: true, message: '请选择入库类型', trigger: 'change' }],
|
||||||
lineIds: [
|
lineIds: [
|
||||||
{
|
{
|
||||||
validator: (_rule, value: string[], callback) => {
|
validator: (_rule, value: string[], callback) => {
|
||||||
@@ -384,6 +400,10 @@ defineExpose({
|
|||||||
grid-template-columns: minmax(0, 1fr);
|
grid-template-columns: minmax(0, 1fr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.form-row-storage {
|
||||||
|
grid-template-columns: minmax(0, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
.form-row-second {
|
.form-row-second {
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
}
|
}
|
||||||
@@ -394,11 +414,13 @@ defineExpose({
|
|||||||
}
|
}
|
||||||
|
|
||||||
.form-item-line-ids,
|
.form-item-line-ids,
|
||||||
|
.form-item-storage-type,
|
||||||
.form-item-interval {
|
.form-item-interval {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-item-line-ids :deep(.el-form-item__content),
|
.form-item-line-ids :deep(.el-form-item__content),
|
||||||
|
.form-item-storage-type :deep(.el-form-item__content),
|
||||||
.form-item-interval :deep(.el-form-item__content) {
|
.form-item-interval :deep(.el-form-item__content) {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,8 @@
|
|||||||
ref="taskPanelRef"
|
ref="taskPanelRef"
|
||||||
:form="taskForm"
|
:form="taskForm"
|
||||||
:preview="previewSummary"
|
:preview="previewSummary"
|
||||||
|
:storage-type-options="storageTypeOptions"
|
||||||
|
:storage-type-loading="loading.storageType"
|
||||||
:preview-loading="loading.preview"
|
:preview-loading="loading.preview"
|
||||||
:submit-loading="loading.create"
|
:submit-loading="loading.create"
|
||||||
:task-running="taskRunning"
|
:task-running="taskRunning"
|
||||||
@@ -36,13 +38,20 @@ import {
|
|||||||
createAddDataTask,
|
createAddDataTask,
|
||||||
getAddDataPreview,
|
getAddDataPreview,
|
||||||
getAddDataTaskStatus,
|
getAddDataTaskStatus,
|
||||||
|
getAddDataStorageTypeList,
|
||||||
getAddDataTemplateList
|
getAddDataTemplateList
|
||||||
} from '@/api/tools/addData'
|
} from '@/api/tools/addData'
|
||||||
import type { AddData } from '@/api/tools/addData/interface'
|
import type { AddData } from '@/api/tools/addData/interface'
|
||||||
import AddDataTaskPanel from './components/AddDataTaskPanel.vue'
|
import AddDataTaskPanel from './components/AddDataTaskPanel.vue'
|
||||||
import AddDataTaskStatusCard from './components/AddDataTaskStatusCard.vue'
|
import AddDataTaskStatusCard from './components/AddDataTaskStatusCard.vue'
|
||||||
import AddDataTemplateTable from './components/AddDataTemplateTable.vue'
|
import AddDataTemplateTable from './components/AddDataTemplateTable.vue'
|
||||||
import { normalizePreview, normalizeTaskStatus, normalizeTemplateItem, resolveText } from './utils/normalize'
|
import {
|
||||||
|
normalizePreview,
|
||||||
|
normalizeStorageTypeOption,
|
||||||
|
normalizeTaskStatus,
|
||||||
|
normalizeTemplateItem,
|
||||||
|
resolveText
|
||||||
|
} from './utils/normalize'
|
||||||
import { buildPayloadSignature, buildTaskPayload as buildTaskRequestPayload } from './utils/taskPayload'
|
import { buildPayloadSignature, buildTaskPayload as buildTaskRequestPayload } from './utils/taskPayload'
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
@@ -53,16 +62,24 @@ type AddDataTaskPanelExpose = {
|
|||||||
validateTaskForm: () => Promise<boolean>
|
validateTaskForm: () => Promise<boolean>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const DEFAULT_STORAGE_TYPE_OPTIONS: AddData.StorageTypeOption[] = [
|
||||||
|
{ code: 'MYSQL', name: 'MySQL' },
|
||||||
|
{ code: 'INFLUXDB', name: 'InfluxDB' }
|
||||||
|
]
|
||||||
|
|
||||||
const taskPanelRef = ref<AddDataTaskPanelExpose | null>(null)
|
const taskPanelRef = ref<AddDataTaskPanelExpose | null>(null)
|
||||||
const activeTab = ref('taskStatus')
|
const activeTab = ref('taskStatus')
|
||||||
|
const storageTypeOptions = ref<AddData.StorageTypeOption[]>([...DEFAULT_STORAGE_TYPE_OPTIONS])
|
||||||
const templateRows = ref<AddData.NormalizedTemplateItem[]>([])
|
const templateRows = ref<AddData.NormalizedTemplateItem[]>([])
|
||||||
const previewSummary = ref<AddData.NormalizedPreview | null>(null)
|
const previewSummary = ref<AddData.NormalizedPreview | null>(null)
|
||||||
const taskStatus = ref<AddData.NormalizedTaskStatus | null>(null)
|
const taskStatus = ref<AddData.NormalizedTaskStatus | null>(null)
|
||||||
const currentTaskId = ref('')
|
const currentTaskId = ref('')
|
||||||
|
const currentTaskStorageType = ref<AddData.StorageType>('MYSQL')
|
||||||
const previewSignature = ref('')
|
const previewSignature = ref('')
|
||||||
const pollTimer = ref<number | null>(null)
|
const pollTimer = ref<number | null>(null)
|
||||||
const pollingBusy = ref(false)
|
const pollingBusy = ref(false)
|
||||||
const loading = reactive({
|
const loading = reactive({
|
||||||
|
storageType: false,
|
||||||
template: false,
|
template: false,
|
||||||
preview: false,
|
preview: false,
|
||||||
create: false,
|
create: false,
|
||||||
@@ -70,6 +87,7 @@ const loading = reactive({
|
|||||||
})
|
})
|
||||||
const taskForm = reactive<AddData.TaskFormModel>({
|
const taskForm = reactive<AddData.TaskFormModel>({
|
||||||
lineMode: 'multiple',
|
lineMode: 'multiple',
|
||||||
|
storageType: 'MYSQL',
|
||||||
lineIds: [],
|
lineIds: [],
|
||||||
startTime: '',
|
startTime: '',
|
||||||
endTime: '',
|
endTime: '',
|
||||||
@@ -78,6 +96,7 @@ const taskForm = reactive<AddData.TaskFormModel>({
|
|||||||
|
|
||||||
const handleTaskFormChange = (nextForm: AddData.TaskFormModel) => {
|
const handleTaskFormChange = (nextForm: AddData.TaskFormModel) => {
|
||||||
taskForm.lineMode = 'multiple'
|
taskForm.lineMode = 'multiple'
|
||||||
|
taskForm.storageType = nextForm.storageType
|
||||||
taskForm.lineIds = [...nextForm.lineIds]
|
taskForm.lineIds = [...nextForm.lineIds]
|
||||||
taskForm.startTime = nextForm.startTime
|
taskForm.startTime = nextForm.startTime
|
||||||
taskForm.endTime = nextForm.endTime
|
taskForm.endTime = nextForm.endTime
|
||||||
@@ -94,7 +113,7 @@ const buildTaskPayload = (): AddData.TaskRequestParams => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const buildPreviewDependencySignature = () => {
|
const buildPreviewDependencySignature = () => {
|
||||||
return buildPayloadSignature(buildTaskPayload())
|
return buildPayloadSignature(buildTaskPayload(), taskForm.storageType)
|
||||||
}
|
}
|
||||||
|
|
||||||
const isTerminalStatus = (status?: AddData.TaskStatus) => {
|
const isTerminalStatus = (status?: AddData.TaskStatus) => {
|
||||||
@@ -106,6 +125,10 @@ const taskRunning = computed(() => {
|
|||||||
return Boolean(currentTaskId.value && (status === 'WAITING' || status === 'RUNNING'))
|
return Boolean(currentTaskId.value && (status === 'WAITING' || status === 'RUNNING'))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const selectedStorageTypeName = computed(() => {
|
||||||
|
return storageTypeOptions.value.find(item => item.code === taskForm.storageType)?.name || taskForm.storageType
|
||||||
|
})
|
||||||
|
|
||||||
const stopPolling = () => {
|
const stopPolling = () => {
|
||||||
if (pollTimer.value !== null) {
|
if (pollTimer.value !== null) {
|
||||||
window.clearInterval(pollTimer.value)
|
window.clearInterval(pollTimer.value)
|
||||||
@@ -113,6 +136,28 @@ const stopPolling = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const loadStorageTypeOptions = async () => {
|
||||||
|
loading.storageType = true
|
||||||
|
try {
|
||||||
|
const response = await getAddDataStorageTypeList()
|
||||||
|
const rows = Array.isArray(response.data) ? response.data : []
|
||||||
|
const options = rows
|
||||||
|
.map(item => normalizeStorageTypeOption(item))
|
||||||
|
.filter((item): item is AddData.StorageTypeOption => Boolean(item))
|
||||||
|
|
||||||
|
storageTypeOptions.value = options.length ? options : [...DEFAULT_STORAGE_TYPE_OPTIONS]
|
||||||
|
|
||||||
|
if (!storageTypeOptions.value.some(item => item.code === taskForm.storageType)) {
|
||||||
|
taskForm.storageType = storageTypeOptions.value[0]?.code || 'MYSQL'
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// 入库类型接口用于联调校验;接口暂不可用时保留默认选项,避免阻断页面补数流程。
|
||||||
|
storageTypeOptions.value = [...DEFAULT_STORAGE_TYPE_OPTIONS]
|
||||||
|
} finally {
|
||||||
|
loading.storageType = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const loadTemplateList = async () => {
|
const loadTemplateList = async () => {
|
||||||
loading.template = true
|
loading.template = true
|
||||||
try {
|
try {
|
||||||
@@ -137,7 +182,11 @@ const getValidatedPayload = async () => {
|
|||||||
return payload
|
return payload
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadTaskStatus = async (taskId = currentTaskId.value, silent = false) => {
|
const loadTaskStatus = async (
|
||||||
|
taskId = currentTaskId.value,
|
||||||
|
silent = false,
|
||||||
|
storageType = currentTaskStorageType.value
|
||||||
|
) => {
|
||||||
if (!taskId || pollingBusy.value) return
|
if (!taskId || pollingBusy.value) return
|
||||||
|
|
||||||
pollingBusy.value = true
|
pollingBusy.value = true
|
||||||
@@ -146,7 +195,7 @@ const loadTaskStatus = async (taskId = currentTaskId.value, silent = false) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await getAddDataTaskStatus(taskId)
|
const response = await getAddDataTaskStatus(taskId, storageType)
|
||||||
// 状态接口是补数任务唯一的进度来源,统一在这里按正式契约归一化,避免页面层分散兼容字段。
|
// 状态接口是补数任务唯一的进度来源,统一在这里按正式契约归一化,避免页面层分散兼容字段。
|
||||||
const normalizedStatus = normalizeTaskStatus(response.data)
|
const normalizedStatus = normalizeTaskStatus(response.data)
|
||||||
taskStatus.value = normalizedStatus
|
taskStatus.value = normalizedStatus
|
||||||
@@ -167,15 +216,16 @@ const loadTaskStatus = async (taskId = currentTaskId.value, silent = false) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const startPolling = (taskId: string) => {
|
const startPolling = (taskId: string, storageType: AddData.StorageType) => {
|
||||||
stopPolling()
|
stopPolling()
|
||||||
currentTaskId.value = taskId
|
currentTaskId.value = taskId
|
||||||
|
currentTaskStorageType.value = storageType
|
||||||
|
|
||||||
// 创建任务后先立即拉一次状态,再进入固定轮询,避免页面长时间停留在初始态。
|
// 创建任务后先立即拉一次状态,再进入固定轮询,避免页面长时间停留在初始态。
|
||||||
void loadTaskStatus(taskId).catch(() => null)
|
void loadTaskStatus(taskId, false, storageType).catch(() => null)
|
||||||
|
|
||||||
pollTimer.value = window.setInterval(() => {
|
pollTimer.value = window.setInterval(() => {
|
||||||
void loadTaskStatus(taskId, true).catch(() => null)
|
void loadTaskStatus(taskId, true, storageType).catch(() => null)
|
||||||
}, 3000)
|
}, 3000)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,7 +238,7 @@ const handlePreview = async () => {
|
|||||||
const response = await getAddDataPreview(payload)
|
const response = await getAddDataPreview(payload)
|
||||||
// preview 是 create 前的唯一准入检查,必须严格按正式契约读取 totalRowCount 和 tableStats。
|
// preview 是 create 前的唯一准入检查,必须严格按正式契约读取 totalRowCount 和 tableStats。
|
||||||
previewSummary.value = normalizePreview(response.data)
|
previewSummary.value = normalizePreview(response.data)
|
||||||
previewSignature.value = buildPayloadSignature(payload)
|
previewSignature.value = buildPayloadSignature(payload, taskForm.storageType)
|
||||||
ElMessage.success('写入规模预估完成')
|
ElMessage.success('写入规模预估完成')
|
||||||
} finally {
|
} finally {
|
||||||
loading.preview = false
|
loading.preview = false
|
||||||
@@ -204,7 +254,7 @@ const handleCreateTask = async () => {
|
|||||||
const payload = await getValidatedPayload()
|
const payload = await getValidatedPayload()
|
||||||
if (!payload) return
|
if (!payload) return
|
||||||
|
|
||||||
const currentSignature = buildPayloadSignature(payload)
|
const currentSignature = buildPayloadSignature(payload, taskForm.storageType)
|
||||||
if (!previewSummary.value || previewSignature.value !== currentSignature) {
|
if (!previewSummary.value || previewSignature.value !== currentSignature) {
|
||||||
ElMessage.warning('参数已变化,请先重新预估写入量')
|
ElMessage.warning('参数已变化,请先重新预估写入量')
|
||||||
return
|
return
|
||||||
@@ -212,7 +262,7 @@ const handleCreateTask = async () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await ElMessageBox.confirm(
|
await ElMessageBox.confirm(
|
||||||
`预计写入 ${previewSummary.value.totalRowCount} 条数据,涉及 ${payload.lineIds.length} 个监测点,确认开始补数?`,
|
`预计向 ${selectedStorageTypeName.value} 写入 ${previewSummary.value.totalRowCount} 条数据,涉及 ${payload.lineIds.length} 个监测点,确认开始补数?`,
|
||||||
'开始补数',
|
'开始补数',
|
||||||
{
|
{
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
@@ -226,7 +276,8 @@ const handleCreateTask = async () => {
|
|||||||
|
|
||||||
loading.create = true
|
loading.create = true
|
||||||
try {
|
try {
|
||||||
const response = await createAddDataTask(payload)
|
const storageType = taskForm.storageType
|
||||||
|
const response = await createAddDataTask(payload, storageType)
|
||||||
const taskId = resolveText(response.data?.taskId)
|
const taskId = resolveText(response.data?.taskId)
|
||||||
|
|
||||||
taskStatus.value = normalizeTaskStatus({
|
taskStatus.value = normalizeTaskStatus({
|
||||||
@@ -239,7 +290,7 @@ const handleCreateTask = async () => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
startPolling(taskId)
|
startPolling(taskId, storageType)
|
||||||
ElMessage.success('补数任务已创建,正在轮询状态')
|
ElMessage.success('补数任务已创建,正在轮询状态')
|
||||||
} finally {
|
} finally {
|
||||||
loading.create = false
|
loading.create = false
|
||||||
@@ -255,7 +306,7 @@ watch(
|
|||||||
)
|
)
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await loadTemplateList()
|
await Promise.allSettled([loadStorageTypeOptions(), loadTemplateList()])
|
||||||
})
|
})
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
|
|||||||
@@ -71,6 +71,20 @@ export const normalizeTaskStatus = (data?: AddData.TaskStatusResponse | null): A
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const normalizeStorageTypeOption = (item: AddData.StorageTypeItem): AddData.StorageTypeOption | null => {
|
||||||
|
const code = resolveText(item.code).toUpperCase() as AddData.StorageType
|
||||||
|
const name = resolveText(item.name, code)
|
||||||
|
|
||||||
|
if (!code) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
code,
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const normalizeTemplateItem = (item: AddData.TemplateItem): AddData.NormalizedTemplateItem => {
|
export const normalizeTemplateItem = (item: AddData.TemplateItem): AddData.NormalizedTemplateItem => {
|
||||||
const decimalScale = resolveText(item.decimalScale)
|
const decimalScale = resolveText(item.decimalScale)
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,9 @@ export const buildTaskPayload = (form: AddData.TaskFormModel): AddData.TaskReque
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const buildPayloadSignature = (payload: AddData.TaskRequestParams) => {
|
export const buildPayloadSignature = (payload: AddData.TaskRequestParams, storageType: AddData.StorageType) => {
|
||||||
return JSON.stringify(payload)
|
return JSON.stringify({
|
||||||
|
storageType,
|
||||||
|
...payload
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,12 +2,12 @@
|
|||||||
<section class="card ledger-form-card">
|
<section class="card ledger-form-card">
|
||||||
<div class="form-header">
|
<div class="form-header">
|
||||||
<div>
|
<div>
|
||||||
<div class="section-title">设备配置</div>
|
<div class="section-title">装置配置</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!readonly" class="form-actions">
|
<div v-if="!readonly" class="form-actions">
|
||||||
<el-button type="primary" :icon="Check" :loading="saving" @click="emit('save')">保存设备</el-button>
|
<el-button type="primary" :icon="Check" :loading="saving" @click="emit('save')">保存装置</el-button>
|
||||||
<el-button type="danger" plain :icon="Delete" :disabled="!localForm.id" @click="emit('delete')">
|
<el-button type="danger" plain :icon="Delete" :disabled="!localForm.id" @click="emit('delete')">
|
||||||
删除设备
|
删除装置
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -23,9 +23,6 @@
|
|||||||
<el-form-item label="装置名称" prop="name">
|
<el-form-item label="装置名称" prop="name">
|
||||||
<el-input v-model="localForm.name" maxlength="80" clearable placeholder="请输入装置名称" />
|
<el-input v-model="localForm.name" maxlength="80" clearable placeholder="请输入装置名称" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item v-if="!isSimpleMode" label="网络设备 ID" prop="ndid">
|
|
||||||
<el-input v-model="localForm.ndid" maxlength="64" clearable placeholder="请输入网络设备 ID" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="装置网络参数" prop="mac">
|
<el-form-item label="装置网络参数" prop="mac">
|
||||||
<el-input v-model="localForm.mac" maxlength="64" clearable placeholder="请输入装置网络参数" />
|
<el-input v-model="localForm.mac" maxlength="64" clearable placeholder="请输入装置网络参数" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@@ -115,13 +112,15 @@ const localForm = reactive<AddLedger.EquipmentForm>({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const syncLocalForm = (form: AddLedger.EquipmentForm) => {
|
const syncLocalForm = (form: AddLedger.EquipmentForm) => {
|
||||||
|
const networkParam = form.mac || form.ndid || ''
|
||||||
|
|
||||||
localForm.id = form.id || ''
|
localForm.id = form.id || ''
|
||||||
localForm.engineeringId = form.engineeringId || ''
|
localForm.engineeringId = form.engineeringId || ''
|
||||||
localForm.projectId = form.projectId || ''
|
localForm.projectId = form.projectId || ''
|
||||||
localForm.parentId = form.parentId || ''
|
localForm.parentId = form.parentId || ''
|
||||||
localForm.name = form.name || ''
|
localForm.name = form.name || ''
|
||||||
localForm.ndid = form.ndid || ''
|
localForm.ndid = networkParam
|
||||||
localForm.mac = form.mac || ''
|
localForm.mac = networkParam
|
||||||
localForm.dev_type = form.dev_type || ''
|
localForm.dev_type = form.dev_type || ''
|
||||||
localForm.dev_model = form.dev_model || ''
|
localForm.dev_model = form.dev_model || ''
|
||||||
localForm.dev_access_method = form.dev_access_method || ''
|
localForm.dev_access_method = form.dev_access_method || ''
|
||||||
@@ -147,7 +146,7 @@ watch(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
emit('update:form', { ...value })
|
emit('update:form', { ...value, ndid: value.mac || '' })
|
||||||
},
|
},
|
||||||
{ deep: true }
|
{ deep: true }
|
||||||
)
|
)
|
||||||
@@ -155,7 +154,6 @@ watch(
|
|||||||
const isSimpleMode = computed(() => props.mode === 'simple')
|
const isSimpleMode = computed(() => props.mode === 'simple')
|
||||||
const formRules = computed<FormRules<AddLedger.EquipmentForm>>(() => ({
|
const formRules = computed<FormRules<AddLedger.EquipmentForm>>(() => ({
|
||||||
name: [{ required: true, message: '请输入装置名称', trigger: 'blur' }],
|
name: [{ required: true, message: '请输入装置名称', trigger: 'blur' }],
|
||||||
...(isSimpleMode.value ? {} : { ndid: [{ required: true, message: '请输入网络设备 ID', trigger: 'blur' }] }),
|
|
||||||
mac: [{ required: true, message: '请输入装置网络参数', trigger: 'blur' }],
|
mac: [{ required: true, message: '请输入装置网络参数', trigger: 'blur' }],
|
||||||
dev_model: [{ required: true, message: '请选择装置型号', trigger: 'change' }]
|
dev_model: [{ required: true, message: '请选择装置型号', trigger: 'change' }]
|
||||||
}))
|
}))
|
||||||
|
|||||||
@@ -15,13 +15,19 @@
|
|||||||
v-if="ledgerContext.engineering || ledgerContext.projects.length"
|
v-if="ledgerContext.engineering || ledgerContext.projects.length"
|
||||||
v-model="projectActiveTab"
|
v-model="projectActiveTab"
|
||||||
addable
|
addable
|
||||||
class="ledger-level-tabs"
|
:class="[
|
||||||
|
'ledger-level-tabs',
|
||||||
|
{
|
||||||
|
'is-add-disabled': !props.canAddProject,
|
||||||
|
'project-add-disabled': !props.canAddProject
|
||||||
|
}
|
||||||
|
]"
|
||||||
@tab-click="pane => handleContextTabClick(pane, 1)"
|
@tab-click="pane => handleContextTabClick(pane, 1)"
|
||||||
@tab-add="emit('add-project')"
|
@tab-add="handleAddProject"
|
||||||
>
|
>
|
||||||
<template #add-icon>
|
<template #add-icon>
|
||||||
<el-tooltip content="新增项目" placement="top">
|
<el-tooltip :content="props.canAddProject ? '新增项目' : props.addProjectDisabledReason" placement="top">
|
||||||
<el-icon class="ledger-tab-add-icon">
|
<el-icon :class="['ledger-tab-add-icon', { 'is-disabled': !props.canAddProject }]">
|
||||||
<CirclePlus />
|
<CirclePlus />
|
||||||
</el-icon>
|
</el-icon>
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
@@ -44,13 +50,22 @@
|
|||||||
v-if="ledgerContext.projects.length || ledgerContext.equipments.length"
|
v-if="ledgerContext.projects.length || ledgerContext.equipments.length"
|
||||||
v-model="equipmentActiveTab"
|
v-model="equipmentActiveTab"
|
||||||
addable
|
addable
|
||||||
class="ledger-level-tabs"
|
:class="[
|
||||||
|
'ledger-level-tabs',
|
||||||
|
{
|
||||||
|
'is-add-disabled': !props.canAddEquipment,
|
||||||
|
'equipment-add-disabled': !props.canAddEquipment
|
||||||
|
}
|
||||||
|
]"
|
||||||
@tab-click="pane => handleContextTabClick(pane, 2)"
|
@tab-click="pane => handleContextTabClick(pane, 2)"
|
||||||
@tab-add="emit('add-equipment')"
|
@tab-add="handleAddEquipment"
|
||||||
>
|
>
|
||||||
<template #add-icon>
|
<template #add-icon>
|
||||||
<el-tooltip content="新增设备" placement="top">
|
<el-tooltip
|
||||||
<el-icon class="ledger-tab-add-icon">
|
:content="props.canAddEquipment ? '新增设备' : props.addEquipmentDisabledReason"
|
||||||
|
placement="top"
|
||||||
|
>
|
||||||
|
<el-icon :class="['ledger-tab-add-icon', { 'is-disabled': !props.canAddEquipment }]">
|
||||||
<CirclePlus />
|
<CirclePlus />
|
||||||
</el-icon>
|
</el-icon>
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
@@ -83,13 +98,19 @@
|
|||||||
v-if="ledgerContext.equipments.length || ledgerContext.lines.length"
|
v-if="ledgerContext.equipments.length || ledgerContext.lines.length"
|
||||||
v-model="lineActiveTab"
|
v-model="lineActiveTab"
|
||||||
addable
|
addable
|
||||||
class="ledger-level-tabs"
|
:class="[
|
||||||
|
'ledger-level-tabs',
|
||||||
|
{
|
||||||
|
'is-add-disabled': !props.canAddLine,
|
||||||
|
'line-add-disabled': !props.canAddLine
|
||||||
|
}
|
||||||
|
]"
|
||||||
@tab-click="pane => handleContextTabClick(pane, 3)"
|
@tab-click="pane => handleContextTabClick(pane, 3)"
|
||||||
@tab-add="emit('add-line')"
|
@tab-add="handleAddLine"
|
||||||
>
|
>
|
||||||
<template #add-icon>
|
<template #add-icon>
|
||||||
<el-tooltip content="新增测点" placement="top">
|
<el-tooltip :content="props.canAddLine ? '新增测点' : props.addLineDisabledReason" placement="top">
|
||||||
<el-icon class="ledger-tab-add-icon">
|
<el-icon :class="['ledger-tab-add-icon', { 'is-disabled': !props.canAddLine }]">
|
||||||
<CirclePlus />
|
<CirclePlus />
|
||||||
</el-icon>
|
</el-icon>
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
@@ -172,6 +193,12 @@ const props = defineProps<{
|
|||||||
deviceModelOptions: AddLedger.SelectOption[]
|
deviceModelOptions: AddLedger.SelectOption[]
|
||||||
lineNoOptions: AddLedger.SelectOption<number>[]
|
lineNoOptions: AddLedger.SelectOption<number>[]
|
||||||
allLineNoOptions: AddLedger.SelectOption<number>[]
|
allLineNoOptions: AddLedger.SelectOption<number>[]
|
||||||
|
canAddProject: boolean
|
||||||
|
canAddEquipment: boolean
|
||||||
|
canAddLine: boolean
|
||||||
|
addProjectDisabledReason: string
|
||||||
|
addEquipmentDisabledReason: string
|
||||||
|
addLineDisabledReason: string
|
||||||
mode: 'simple' | 'full'
|
mode: 'simple' | 'full'
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
@@ -263,6 +290,24 @@ const handleContextTabClick = (pane: TabsPaneContext, level: AddLedger.NodeLevel
|
|||||||
emit('tab-click', id, level)
|
emit('tab-click', id, level)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleAddProject = () => {
|
||||||
|
if (!props.canAddProject) return
|
||||||
|
|
||||||
|
emit('add-project')
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAddEquipment = () => {
|
||||||
|
if (!props.canAddEquipment) return
|
||||||
|
|
||||||
|
emit('add-equipment')
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAddLine = () => {
|
||||||
|
if (!props.canAddLine) return
|
||||||
|
|
||||||
|
emit('add-line')
|
||||||
|
}
|
||||||
|
|
||||||
const validateActiveForm = async (level: AddLedger.NodeLevel | null) => {
|
const validateActiveForm = async (level: AddLedger.NodeLevel | null) => {
|
||||||
if (level === 0) return Boolean(await engineeringFormRef.value?.validateForm())
|
if (level === 0) return Boolean(await engineeringFormRef.value?.validateForm())
|
||||||
if (level === 1) return Boolean(await projectFormRef.value?.validateForm())
|
if (level === 1) return Boolean(await projectFormRef.value?.validateForm())
|
||||||
@@ -320,10 +365,22 @@ defineExpose({
|
|||||||
border-color: var(--el-color-primary-light-3);
|
border-color: var(--el-color-primary-light-3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ledger-level-tabs.is-add-disabled :deep(.el-tabs__new-tab),
|
||||||
|
.ledger-level-tabs.is-add-disabled :deep(.el-tabs__new-tab:hover) {
|
||||||
|
color: var(--el-text-color-disabled);
|
||||||
|
cursor: not-allowed;
|
||||||
|
background-color: var(--el-fill-color-light);
|
||||||
|
border-color: var(--el-border-color);
|
||||||
|
}
|
||||||
|
|
||||||
.ledger-tab-add-icon {
|
.ledger-tab-add-icon {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ledger-tab-add-icon.is-disabled {
|
||||||
|
color: var(--el-text-color-disabled);
|
||||||
|
}
|
||||||
|
|
||||||
.ledger-level-tabs :deep(.el-tabs__content) {
|
.ledger-level-tabs :deep(.el-tabs__content) {
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
:rules="formRules"
|
:rules="formRules"
|
||||||
:disabled="readonly"
|
:disabled="readonly"
|
||||||
label-width="146px"
|
label-width="146px"
|
||||||
:class="['ledger-form', isSimpleMode ? 'form-simple' : 'form-three']"
|
:class="['ledger-form', 'ledger-line-form', isSimpleMode ? 'form-simple' : 'form-three']"
|
||||||
>
|
>
|
||||||
<el-form-item label="监测点名" prop="name">
|
<el-form-item label="监测点名" prop="name">
|
||||||
<el-input v-model="localForm.name" maxlength="80" clearable placeholder="请输入监测点名" />
|
<el-input v-model="localForm.name" maxlength="80" clearable placeholder="请输入监测点名" />
|
||||||
|
|||||||
@@ -43,17 +43,24 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.ledger-form :deep(.el-form-item) {
|
.ledger-form :deep(.el-form-item) {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
min-height: 32px;
|
||||||
margin-right: 0 !important;
|
margin-right: 0 !important;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ledger-form :deep(.el-form-item__label) {
|
.ledger-form :deep(.el-form-item__label) {
|
||||||
|
height: 32px;
|
||||||
line-height: 32px;
|
line-height: 32px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ledger-form :deep(.el-form-item__content) {
|
.ledger-form :deep(.el-form-item__content) {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
min-height: 32px;
|
||||||
line-height: 32px;
|
line-height: 32px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,6 +71,11 @@
|
|||||||
gap: 5px 14px;
|
gap: 5px 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ledger-form.ledger-line-form {
|
||||||
|
grid-auto-rows: minmax(32px, auto);
|
||||||
|
row-gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
.ledger-form.form-three :deep(.el-form-item),
|
.ledger-form.form-three :deep(.el-form-item),
|
||||||
.ledger-form.form-simple :deep(.el-form-item) {
|
.ledger-form.form-simple :deep(.el-form-item) {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -82,6 +94,10 @@
|
|||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ledger-form :deep(.form-item-wide > .el-form-item__content) {
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
.ratio-input-group {
|
.ratio-input-group {
|
||||||
display: flex;
|
display: flex;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -95,6 +111,11 @@
|
|||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ledger-line-form .ratio-input-group :deep(.ratio-field) {
|
||||||
|
min-height: 32px;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.ratio-input-group :deep(.ratio-field .el-form-item__content) {
|
.ratio-input-group :deep(.ratio-field .el-form-item__content) {
|
||||||
display: block;
|
display: block;
|
||||||
line-height: 32px;
|
line-height: 32px;
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
/* eslint-env node */
|
||||||
|
import fs from 'node:fs'
|
||||||
|
import path from 'node:path'
|
||||||
|
import { fileURLToPath } from 'node:url'
|
||||||
|
|
||||||
|
const currentDir = path.dirname(fileURLToPath(import.meta.url))
|
||||||
|
const pageDir = path.resolve(currentDir, '..')
|
||||||
|
|
||||||
|
const indexSource = fs.readFileSync(path.join(pageDir, 'index.vue'), 'utf8')
|
||||||
|
const contextPanelSource = fs.readFileSync(path.join(pageDir, 'components', 'LedgerContextPanel.vue'), 'utf8')
|
||||||
|
|
||||||
|
const expectations = [
|
||||||
|
[
|
||||||
|
'index passes add availability props',
|
||||||
|
/:can-add-project="canAddProject"/.test(indexSource) &&
|
||||||
|
/:can-add-equipment="canAddEquipment"/.test(indexSource) &&
|
||||||
|
/:can-add-line="canAddLine"/.test(indexSource) &&
|
||||||
|
/:add-project-disabled-reason="addProjectDisabledReason"/.test(indexSource) &&
|
||||||
|
/:add-equipment-disabled-reason="addEquipmentDisabledReason"/.test(indexSource) &&
|
||||||
|
/:add-line-disabled-reason="addLineDisabledReason"/.test(indexSource)
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'index computes add availability from saved parents',
|
||||||
|
/const resolveEngineeringIdForAddProject = \(\) =>/.test(indexSource) &&
|
||||||
|
/const resolveProjectContextForAddEquipment = \(\) =>/.test(indexSource) &&
|
||||||
|
/const resolveDeviceIdForAddLine = \(\) =>/.test(indexSource) &&
|
||||||
|
/const canAddProject = computed\(\(\) => Boolean\(resolveEngineeringIdForAddProject\(\)\)\)/.test(indexSource) &&
|
||||||
|
/const canAddEquipment = computed\(\(\) => Boolean\(resolveProjectContextForAddEquipment\(\)\.projectId\)\)/.test(
|
||||||
|
indexSource
|
||||||
|
) &&
|
||||||
|
/const canAddLine = computed\(\(\) => Boolean\(resolveDeviceIdForAddLine\(\)\)\)/.test(indexSource)
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'context panel declares availability props',
|
||||||
|
/canAddProject: boolean/.test(contextPanelSource) &&
|
||||||
|
/canAddEquipment: boolean/.test(contextPanelSource) &&
|
||||||
|
/canAddLine: boolean/.test(contextPanelSource) &&
|
||||||
|
/addProjectDisabledReason: string/.test(contextPanelSource) &&
|
||||||
|
/addEquipmentDisabledReason: string/.test(contextPanelSource) &&
|
||||||
|
/addLineDisabledReason: string/.test(contextPanelSource)
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'context panel blocks disabled add events',
|
||||||
|
/const handleAddProject = \(\) => \{[\s\S]*if \(!props\.canAddProject\) return[\s\S]*emit\('add-project'\)/.test(
|
||||||
|
contextPanelSource
|
||||||
|
) &&
|
||||||
|
/const handleAddEquipment = \(\) => \{[\s\S]*if \(!props\.canAddEquipment\) return[\s\S]*emit\('add-equipment'\)/.test(
|
||||||
|
contextPanelSource
|
||||||
|
) &&
|
||||||
|
/const handleAddLine = \(\) => \{[\s\S]*if \(!props\.canAddLine\) return[\s\S]*emit\('add-line'\)/.test(
|
||||||
|
contextPanelSource
|
||||||
|
)
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'context panel marks disabled add controls',
|
||||||
|
/project-add-disabled/.test(contextPanelSource) &&
|
||||||
|
/equipment-add-disabled/.test(contextPanelSource) &&
|
||||||
|
/line-add-disabled/.test(contextPanelSource) &&
|
||||||
|
/ledger-tab-add-icon/.test(contextPanelSource) &&
|
||||||
|
/'is-disabled':\s*!props\.canAddProject/.test(contextPanelSource) &&
|
||||||
|
/'is-disabled':\s*!props\.canAddEquipment/.test(contextPanelSource) &&
|
||||||
|
/'is-disabled':\s*!props\.canAddLine/.test(contextPanelSource) &&
|
||||||
|
/\.ledger-level-tabs\.is-add-disabled/.test(contextPanelSource)
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
const failures = expectations.filter(([, matched]) => !matched)
|
||||||
|
|
||||||
|
if (failures.length) {
|
||||||
|
console.error('addLedger add button availability contract check failed:')
|
||||||
|
for (const [name] of failures) {
|
||||||
|
console.error(`- ${name}`)
|
||||||
|
}
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('addLedger add button availability contract check passed')
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
/* eslint-env node */
|
||||||
|
import fs from 'node:fs'
|
||||||
|
import path from 'node:path'
|
||||||
|
import { fileURLToPath } from 'node:url'
|
||||||
|
|
||||||
|
const currentDir = path.dirname(fileURLToPath(import.meta.url))
|
||||||
|
const pageDir = path.resolve(currentDir, '..')
|
||||||
|
|
||||||
|
const indexSource = fs.readFileSync(path.join(pageDir, 'index.vue'), 'utf8')
|
||||||
|
|
||||||
|
const expectations = [
|
||||||
|
[
|
||||||
|
'add equipment resolves first device type option as default',
|
||||||
|
/const resolveDefaultDeviceType = \(\) => \{[\s\S]*const defaultDeviceType = deviceTypeOptions\.value\[0\]\?\.value[\s\S]*return defaultDeviceType === undefined \|\| defaultDeviceType === null \? '' : String\(defaultDeviceType\)[\s\S]*\}/.test(
|
||||||
|
indexSource
|
||||||
|
)
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'add equipment applies default device type to draft form',
|
||||||
|
/equipmentForm\.value = \{[\s\S]*\.\.\.createEmptyEquipmentForm\(projectId,\s*engineeringId\),[\s\S]*dev_type:\s*resolveDefaultDeviceType\(\)[\s\S]*\}/.test(
|
||||||
|
indexSource
|
||||||
|
)
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
const failures = expectations.filter(([, matched]) => !matched)
|
||||||
|
|
||||||
|
if (failures.length) {
|
||||||
|
console.error('addLedger equipment default contract check failed:')
|
||||||
|
for (const [name] of failures) {
|
||||||
|
console.error(`- ${name}`)
|
||||||
|
}
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('addLedger equipment default contract check passed')
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
/* eslint-env node */
|
||||||
|
import fs from 'node:fs'
|
||||||
|
import path from 'node:path'
|
||||||
|
import { fileURLToPath } from 'node:url'
|
||||||
|
|
||||||
|
const currentDir = path.dirname(fileURLToPath(import.meta.url))
|
||||||
|
const pageDir = path.resolve(currentDir, '..')
|
||||||
|
|
||||||
|
const equipmentFormSource = fs.readFileSync(path.join(pageDir, 'components', 'EquipmentForm.vue'), 'utf8')
|
||||||
|
const ledgerDataSource = fs.readFileSync(path.join(pageDir, 'utils', 'ledgerData.ts'), 'utf8')
|
||||||
|
|
||||||
|
const expectations = [
|
||||||
|
[
|
||||||
|
'equipment form labels equipment as device unit',
|
||||||
|
/<div class="section-title">装置配置<\/div>/.test(equipmentFormSource) &&
|
||||||
|
/保存装置/.test(equipmentFormSource) &&
|
||||||
|
/删除装置/.test(equipmentFormSource) &&
|
||||||
|
/label="装置名称"[\s\S]*placeholder="请输入装置名称"/.test(equipmentFormSource) &&
|
||||||
|
/message:\s*'请输入装置名称'/.test(equipmentFormSource)
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'equipment form hides independent network device id input',
|
||||||
|
!/label="网络设备 ID"/.test(equipmentFormSource) &&
|
||||||
|
!/placeholder="请输入网络设备 ID"/.test(equipmentFormSource) &&
|
||||||
|
!/ndid:\s*\[\{ required:\s*true/.test(equipmentFormSource)
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'equipment form emits ndid from network param',
|
||||||
|
/const networkParam = form\.mac \|\| form\.ndid \|\| ''/.test(equipmentFormSource) &&
|
||||||
|
/localForm\.ndid = networkParam/.test(equipmentFormSource) &&
|
||||||
|
/localForm\.mac = networkParam/.test(equipmentFormSource) &&
|
||||||
|
/emit\('update:form', \{ \.\.\.value, ndid: value\.mac \|\| '' \}\)/.test(equipmentFormSource)
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'equipment detail normalizes name and network param from compatible backend fields',
|
||||||
|
/const equipmentName = resolveString\(data, 'name', 'dev_name', 'devName', 'deviceName'\) \|\| node\?\.name \|\| ''/.test(
|
||||||
|
ledgerDataSource
|
||||||
|
) &&
|
||||||
|
/const networkParam = resolveString\(data, 'mac', 'ndid', 'unnid'\)/.test(ledgerDataSource) &&
|
||||||
|
/name:\s*equipmentName/.test(ledgerDataSource) &&
|
||||||
|
/ndid:\s*networkParam/.test(ledgerDataSource) &&
|
||||||
|
/mac:\s*networkParam/.test(ledgerDataSource)
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
const failures = expectations.filter(([, matched]) => !matched)
|
||||||
|
|
||||||
|
if (failures.length) {
|
||||||
|
console.error('addLedger equipment network param contract check failed:')
|
||||||
|
for (const [name] of failures) {
|
||||||
|
console.error(`- ${name}`)
|
||||||
|
}
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('addLedger equipment network param contract check passed')
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
/* eslint-env node */
|
||||||
|
import fs from 'node:fs'
|
||||||
|
import path from 'node:path'
|
||||||
|
import { fileURLToPath } from 'node:url'
|
||||||
|
|
||||||
|
const currentDir = path.dirname(fileURLToPath(import.meta.url))
|
||||||
|
const pageDir = path.resolve(currentDir, '..')
|
||||||
|
|
||||||
|
const formStyleSource = fs.readFileSync(path.join(pageDir, 'components', 'ledgerForm.scss'), 'utf8')
|
||||||
|
const lineFormSource = fs.readFileSync(path.join(pageDir, 'components', 'LineForm.vue'), 'utf8')
|
||||||
|
|
||||||
|
const expectations = [
|
||||||
|
[
|
||||||
|
'form item uses a fixed single-line alignment baseline',
|
||||||
|
/\.ledger-form :deep\(\.el-form-item\) \{[\s\S]*display:\s*flex;[\s\S]*align-items:\s*flex-start;[\s\S]*min-height:\s*32px;[\s\S]*\}/.test(
|
||||||
|
formStyleSource
|
||||||
|
)
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'form label has explicit 32px height',
|
||||||
|
/\.ledger-form :deep\(\.el-form-item__label\) \{[\s\S]*height:\s*32px;[\s\S]*line-height:\s*32px;[\s\S]*\}/.test(
|
||||||
|
formStyleSource
|
||||||
|
)
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'form content centers single-line controls on the 32px baseline',
|
||||||
|
/\.ledger-form :deep\(\.el-form-item__content\) \{[\s\S]*display:\s*flex;[\s\S]*align-items:\s*center;[\s\S]*min-height:\s*32px;[\s\S]*line-height:\s*32px;[\s\S]*\}/.test(
|
||||||
|
formStyleSource
|
||||||
|
)
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'textarea keeps top alignment instead of inheriting centered single-line controls',
|
||||||
|
/\.ledger-form :deep\(\.form-item-wide > \.el-form-item__content\) \{[\s\S]*align-items:\s*flex-start;[\s\S]*\}/.test(
|
||||||
|
formStyleSource
|
||||||
|
)
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'ratio fields keep block layout for nested validation items',
|
||||||
|
/\.ratio-input-group :deep\(\.ratio-field \.el-form-item__content\) \{[\s\S]*display:\s*block;[\s\S]*\}/.test(
|
||||||
|
formStyleSource
|
||||||
|
)
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'line form has a dedicated class for monitor point spacing',
|
||||||
|
/'ledger-line-form'/.test(lineFormSource)
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'line form uses explicit grid row sizing and row gap',
|
||||||
|
/\.ledger-form\.ledger-line-form \{[\s\S]*grid-auto-rows:\s*minmax\(32px,\s*auto\);[\s\S]*row-gap:\s*2px;[\s\S]*\}/.test(
|
||||||
|
formStyleSource
|
||||||
|
)
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'line form ratio field spacing is normalized',
|
||||||
|
/\.ledger-line-form \.ratio-input-group :deep\(\.ratio-field\) \{[\s\S]*min-height:\s*32px;[\s\S]*margin-bottom:\s*0;[\s\S]*\}/.test(
|
||||||
|
formStyleSource
|
||||||
|
)
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
const failures = expectations.filter(([, matched]) => !matched)
|
||||||
|
|
||||||
|
if (failures.length) {
|
||||||
|
console.error('addLedger form alignment contract check failed:')
|
||||||
|
for (const [name] of failures) {
|
||||||
|
console.error(`- ${name}`)
|
||||||
|
}
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('addLedger form alignment contract check passed')
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
/* eslint-env node */
|
||||||
|
import fs from 'node:fs'
|
||||||
|
import path from 'node:path'
|
||||||
|
import { fileURLToPath } from 'node:url'
|
||||||
|
|
||||||
|
const currentDir = path.dirname(fileURLToPath(import.meta.url))
|
||||||
|
const pageDir = path.resolve(currentDir, '..')
|
||||||
|
const srcDir = path.resolve(pageDir, '..', '..', '..')
|
||||||
|
|
||||||
|
const constantsSource = fs.readFileSync(path.join(srcDir, 'constants', 'dictCodes.ts'), 'utf8')
|
||||||
|
const indexSource = fs.readFileSync(path.join(pageDir, 'index.vue'), 'utf8')
|
||||||
|
const equipmentFormSource = fs.readFileSync(path.join(pageDir, 'components', 'EquipmentForm.vue'), 'utf8')
|
||||||
|
|
||||||
|
const expectations = [
|
||||||
|
['dict code defines terminal model', /LEDGER_TERMINAL_MODEL:\s*'Dev_Type'/.test(constantsSource)],
|
||||||
|
[
|
||||||
|
'addLedger uses terminal model dict for equipment model options',
|
||||||
|
/typeof DICT_CODES\.LEDGER_TERMINAL_MODEL/.test(indexSource) &&
|
||||||
|
/resolveDictOptions\(DICT_CODES\.LEDGER_TERMINAL_MODEL,\s*fallbackDeviceModelOptions\)/.test(indexSource) &&
|
||||||
|
/ensureLedgerDictOptionById\(DICT_CODES\.LEDGER_TERMINAL_MODEL,\s*form\.dev_model\s*\|\|\s*''\)/.test(
|
||||||
|
indexSource
|
||||||
|
) &&
|
||||||
|
/loadLedgerDictOptionsByCode\(DICT_CODES\.LEDGER_TERMINAL_MODEL\)/.test(indexSource)
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'equipment form labels terminal dict as device model',
|
||||||
|
/label="装置型号"[\s\S]*placeholder="请选择装置型号"/.test(equipmentFormSource) &&
|
||||||
|
/dev_model:\s*\[\{ required:\s*true,\s*message:\s*'请选择装置型号'/.test(equipmentFormSource)
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
const failures = expectations.filter(([, matched]) => !matched)
|
||||||
|
|
||||||
|
if (failures.length) {
|
||||||
|
console.error('addLedger terminal model contract check failed:')
|
||||||
|
for (const [name] of failures) {
|
||||||
|
console.error(`- ${name}`)
|
||||||
|
}
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('addLedger terminal model contract check passed')
|
||||||
@@ -43,6 +43,12 @@
|
|||||||
:device-model-options="deviceModelOptions"
|
:device-model-options="deviceModelOptions"
|
||||||
:line-no-options="lineNoOptions"
|
:line-no-options="lineNoOptions"
|
||||||
:all-line-no-options="allLineNoOptions"
|
:all-line-no-options="allLineNoOptions"
|
||||||
|
:can-add-project="canAddProject"
|
||||||
|
:can-add-equipment="canAddEquipment"
|
||||||
|
:can-add-line="canAddLine"
|
||||||
|
:add-project-disabled-reason="addProjectDisabledReason"
|
||||||
|
:add-equipment-disabled-reason="addEquipmentDisabledReason"
|
||||||
|
:add-line-disabled-reason="addLineDisabledReason"
|
||||||
:mode="ledgerFormMode"
|
:mode="ledgerFormMode"
|
||||||
@save-engineering="handleSaveEngineering"
|
@save-engineering="handleSaveEngineering"
|
||||||
@save-project="handleSaveProject"
|
@save-project="handleSaveProject"
|
||||||
@@ -100,7 +106,6 @@ import {
|
|||||||
createEmptyLineForm,
|
createEmptyLineForm,
|
||||||
createEmptyProjectForm,
|
createEmptyProjectForm,
|
||||||
findNodePath,
|
findNodePath,
|
||||||
generateGuidText,
|
|
||||||
normalizeEngineeringDetail,
|
normalizeEngineeringDetail,
|
||||||
normalizeEquipmentDetail,
|
normalizeEquipmentDetail,
|
||||||
normalizeLineDetail,
|
normalizeLineDetail,
|
||||||
@@ -127,7 +132,7 @@ type LedgerContextItem<T> = {
|
|||||||
draft?: boolean
|
draft?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
type LedgerDictCode = typeof DICT_CODES.LEDGER_DEVICE_TYPE | typeof DICT_CODES.LEDGER_DEVICE_MODEL
|
type LedgerDictCode = typeof DICT_CODES.LEDGER_DEVICE_TYPE | typeof DICT_CODES.LEDGER_TERMINAL_MODEL
|
||||||
|
|
||||||
const dictStore = useDictStore()
|
const dictStore = useDictStore()
|
||||||
const treeData = ref<AddLedger.NormalizedTreeNode[]>([])
|
const treeData = ref<AddLedger.NormalizedTreeNode[]>([])
|
||||||
@@ -171,7 +176,7 @@ const activeTabIds = reactive({
|
|||||||
})
|
})
|
||||||
const ledgerDictOptions = reactive<Record<LedgerDictCode, AddLedger.SelectOption[]>>({
|
const ledgerDictOptions = reactive<Record<LedgerDictCode, AddLedger.SelectOption[]>>({
|
||||||
[DICT_CODES.LEDGER_DEVICE_TYPE]: [],
|
[DICT_CODES.LEDGER_DEVICE_TYPE]: [],
|
||||||
[DICT_CODES.LEDGER_DEVICE_MODEL]: []
|
[DICT_CODES.LEDGER_TERMINAL_MODEL]: []
|
||||||
})
|
})
|
||||||
|
|
||||||
let detailRequestSeq = 0
|
let detailRequestSeq = 0
|
||||||
@@ -188,11 +193,19 @@ const fallbackDeviceModelOptions: AddLedger.SelectOption[] = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
const deviceTypeOptions = computed(() => resolveDictOptions(DICT_CODES.LEDGER_DEVICE_TYPE, fallbackDeviceTypeOptions))
|
const deviceTypeOptions = computed(() => resolveDictOptions(DICT_CODES.LEDGER_DEVICE_TYPE, fallbackDeviceTypeOptions))
|
||||||
const deviceModelOptions = computed(() => resolveDictOptions(DICT_CODES.LEDGER_DEVICE_MODEL, fallbackDeviceModelOptions))
|
const deviceModelOptions = computed(() =>
|
||||||
|
resolveDictOptions(DICT_CODES.LEDGER_TERMINAL_MODEL, fallbackDeviceModelOptions)
|
||||||
|
)
|
||||||
const emptyStateText = computed(() =>
|
const emptyStateText = computed(() =>
|
||||||
treeData.value.length === 0 ? '台账树为空,请先新增一个工程。' : '从左侧台账树选择工程、项目、设备或监测点。'
|
treeData.value.length === 0 ? '台账树为空,请先新增一个工程。' : '从左侧台账树选择工程、项目、设备或监测点。'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const resolveDefaultDeviceType = () => {
|
||||||
|
const defaultDeviceType = deviceTypeOptions.value[0]?.value
|
||||||
|
|
||||||
|
return defaultDeviceType === undefined || defaultDeviceType === null ? '' : String(defaultDeviceType)
|
||||||
|
}
|
||||||
|
|
||||||
const resolveFallbackDictOptions = (code: LedgerDictCode) =>
|
const resolveFallbackDictOptions = (code: LedgerDictCode) =>
|
||||||
code === DICT_CODES.LEDGER_DEVICE_TYPE ? fallbackDeviceTypeOptions : fallbackDeviceModelOptions
|
code === DICT_CODES.LEDGER_DEVICE_TYPE ? fallbackDeviceTypeOptions : fallbackDeviceModelOptions
|
||||||
|
|
||||||
@@ -238,7 +251,7 @@ const ensureLedgerDictOptionById = async (code: LedgerDictCode, value: string) =
|
|||||||
const ensureEquipmentDictOptions = async (form: AddLedger.EquipmentForm) => {
|
const ensureEquipmentDictOptions = async (form: AddLedger.EquipmentForm) => {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
ensureLedgerDictOptionById(DICT_CODES.LEDGER_DEVICE_TYPE, form.dev_type || ''),
|
ensureLedgerDictOptionById(DICT_CODES.LEDGER_DEVICE_TYPE, form.dev_type || ''),
|
||||||
ensureLedgerDictOptionById(DICT_CODES.LEDGER_DEVICE_MODEL, form.dev_model || '')
|
ensureLedgerDictOptionById(DICT_CODES.LEDGER_TERMINAL_MODEL, form.dev_model || '')
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -254,7 +267,7 @@ const loadLedgerDictOptionsByCode = (code: LedgerDictCode) => {
|
|||||||
|
|
||||||
const loadLedgerDictOptions = async () => {
|
const loadLedgerDictOptions = async () => {
|
||||||
loadLedgerDictOptionsByCode(DICT_CODES.LEDGER_DEVICE_TYPE)
|
loadLedgerDictOptionsByCode(DICT_CODES.LEDGER_DEVICE_TYPE)
|
||||||
loadLedgerDictOptionsByCode(DICT_CODES.LEDGER_DEVICE_MODEL)
|
loadLedgerDictOptionsByCode(DICT_CODES.LEDGER_TERMINAL_MODEL)
|
||||||
}
|
}
|
||||||
|
|
||||||
const getCurrentPath = () => {
|
const getCurrentPath = () => {
|
||||||
@@ -268,7 +281,52 @@ const resolveContext = () => {
|
|||||||
return resolveContextFromPath(path)
|
return resolveContextFromPath(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const resolveEngineeringIdForAddProject = () => {
|
||||||
|
const context = resolveContext()
|
||||||
|
const engineeringIdFromContext = resolveSavedContextItemId(ledgerContext.engineering)
|
||||||
|
|
||||||
|
return activeLevel.value === 0
|
||||||
|
? engineeringForm.value.id || selectedNode.value?.id || ''
|
||||||
|
: context.engineeringId || engineeringIdFromContext
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolveProjectContextForAddEquipment = () => {
|
||||||
|
const context = resolveContext()
|
||||||
|
const activeProjectItem = ledgerContext.projects.find(item => item.id === activeTabIds.project)
|
||||||
|
const projectIdFromTab = resolveSavedContextItemId(activeProjectItem)
|
||||||
|
const projectId =
|
||||||
|
activeLevel.value === 1
|
||||||
|
? projectForm.value.id || selectedNode.value?.id || ''
|
||||||
|
: context.projectId || projectIdFromTab
|
||||||
|
const engineeringId =
|
||||||
|
projectForm.value.engineeringId ||
|
||||||
|
context.engineeringId ||
|
||||||
|
activeProjectItem?.form.engineeringId ||
|
||||||
|
resolveSavedContextItemId(ledgerContext.engineering)
|
||||||
|
|
||||||
|
return {
|
||||||
|
projectId,
|
||||||
|
engineeringId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolveDeviceIdForAddLine = () => {
|
||||||
|
const context = resolveContext()
|
||||||
|
const activeEquipmentItem = ledgerContext.equipments.find(item => item.id === activeTabIds.equipment)
|
||||||
|
const deviceIdFromTab = resolveSavedContextItemId(activeEquipmentItem)
|
||||||
|
|
||||||
|
return activeLevel.value === 2
|
||||||
|
? equipmentForm.value.id || selectedNode.value?.id || ''
|
||||||
|
: context.deviceId || deviceIdFromTab
|
||||||
|
}
|
||||||
|
|
||||||
const allLineNoOptions = computed(() => buildLineNoOptions(Array.from({ length: 20 }, (_item, index) => index + 1)))
|
const allLineNoOptions = computed(() => buildLineNoOptions(Array.from({ length: 20 }, (_item, index) => index + 1)))
|
||||||
|
const canAddProject = computed(() => Boolean(resolveEngineeringIdForAddProject()))
|
||||||
|
const canAddEquipment = computed(() => Boolean(resolveProjectContextForAddEquipment().projectId))
|
||||||
|
const canAddLine = computed(() => Boolean(resolveDeviceIdForAddLine()))
|
||||||
|
const addProjectDisabledReason = computed(() => (canAddProject.value ? '' : '请先选择或保存父级工程'))
|
||||||
|
const addEquipmentDisabledReason = computed(() => (canAddEquipment.value ? '' : '请先选择或保存父级项目'))
|
||||||
|
const addLineDisabledReason = computed(() => (canAddLine.value ? '' : '请先选择或保存父级设备'))
|
||||||
|
|
||||||
const loadAvailableLineNoOptions = async (deviceId: string, lineId = '', currentLineNo?: number) => {
|
const loadAvailableLineNoOptions = async (deviceId: string, lineId = '', currentLineNo?: number) => {
|
||||||
if (!deviceId) {
|
if (!deviceId) {
|
||||||
@@ -637,15 +695,10 @@ const handleAddEngineering = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleAddProject = () => {
|
const handleAddProject = () => {
|
||||||
const context = resolveContext()
|
const engineeringId = resolveEngineeringIdForAddProject()
|
||||||
const engineeringIdFromContext = resolveSavedContextItemId(ledgerContext.engineering)
|
|
||||||
const engineeringId =
|
|
||||||
activeLevel.value === 0
|
|
||||||
? engineeringForm.value.id || selectedNode.value?.id || ''
|
|
||||||
: context.engineeringId || engineeringIdFromContext
|
|
||||||
|
|
||||||
if (!engineeringId) {
|
if (!engineeringId) {
|
||||||
ElMessage.warning('请先选择或保存父级工程')
|
ElMessage.warning(addProjectDisabledReason.value)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -662,27 +715,19 @@ const handleAddProject = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleAddEquipment = () => {
|
const handleAddEquipment = () => {
|
||||||
const context = resolveContext()
|
const { projectId, engineeringId } = resolveProjectContextForAddEquipment()
|
||||||
const activeProjectItem = ledgerContext.projects.find(item => item.id === activeTabIds.project)
|
|
||||||
const projectIdFromTab = resolveSavedContextItemId(activeProjectItem)
|
|
||||||
const projectId =
|
|
||||||
activeLevel.value === 1
|
|
||||||
? projectForm.value.id || selectedNode.value?.id || ''
|
|
||||||
: context.projectId || projectIdFromTab
|
|
||||||
const engineeringId =
|
|
||||||
projectForm.value.engineeringId ||
|
|
||||||
context.engineeringId ||
|
|
||||||
activeProjectItem?.form.engineeringId ||
|
|
||||||
resolveSavedContextItemId(ledgerContext.engineering)
|
|
||||||
|
|
||||||
if (!projectId) {
|
if (!projectId) {
|
||||||
ElMessage.warning('请先选择或保存父级项目')
|
ElMessage.warning(addEquipmentDisabledReason.value)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
selectedNode.value = null
|
selectedNode.value = null
|
||||||
activeLevel.value = 2
|
activeLevel.value = 2
|
||||||
equipmentForm.value = createEmptyEquipmentForm(projectId, engineeringId)
|
equipmentForm.value = {
|
||||||
|
...createEmptyEquipmentForm(projectId, engineeringId),
|
||||||
|
dev_type: resolveDefaultDeviceType()
|
||||||
|
}
|
||||||
ledgerContext.equipments = [
|
ledgerContext.equipments = [
|
||||||
...ledgerContext.equipments.filter(item => !item.draft),
|
...ledgerContext.equipments.filter(item => !item.draft),
|
||||||
createDraftContextItem(draftIds.equipment, '新增设备', 2, equipmentForm.value)
|
createDraftContextItem(draftIds.equipment, '新增设备', 2, equipmentForm.value)
|
||||||
@@ -692,16 +737,10 @@ const handleAddEquipment = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleAddLine = async () => {
|
const handleAddLine = async () => {
|
||||||
const context = resolveContext()
|
const deviceId = resolveDeviceIdForAddLine()
|
||||||
const activeEquipmentItem = ledgerContext.equipments.find(item => item.id === activeTabIds.equipment)
|
|
||||||
const deviceIdFromTab = resolveSavedContextItemId(activeEquipmentItem)
|
|
||||||
const deviceId =
|
|
||||||
activeLevel.value === 2
|
|
||||||
? equipmentForm.value.id || selectedNode.value?.id || ''
|
|
||||||
: context.deviceId || deviceIdFromTab
|
|
||||||
|
|
||||||
if (!deviceId) {
|
if (!deviceId) {
|
||||||
ElMessage.warning('请先选择或保存父级设备')
|
ElMessage.warning(addLineDisabledReason.value)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -797,11 +836,6 @@ const handleSaveLine = async () => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!lineForm.value.line_id) {
|
|
||||||
// 新增测点必须带 32 位 line_id,后端仍负责最终唯一性校验。
|
|
||||||
lineForm.value.line_id = generateGuidText()
|
|
||||||
}
|
|
||||||
|
|
||||||
loading.saving = true
|
loading.saving = true
|
||||||
try {
|
try {
|
||||||
const response = await saveAddLedgerLine(lineForm.value)
|
const response = await saveAddLedgerLine(lineForm.value)
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ export function createEmptyEquipmentForm(parentProjectId = '', parentEngineering
|
|||||||
export function createEmptyLineForm(parentDeviceId = ''): AddLedger.LineForm {
|
export function createEmptyLineForm(parentDeviceId = ''): AddLedger.LineForm {
|
||||||
return {
|
return {
|
||||||
id: '',
|
id: '',
|
||||||
line_id: generateGuidText(),
|
line_id: '',
|
||||||
deviceId: parentDeviceId,
|
deviceId: parentDeviceId,
|
||||||
parentId: parentDeviceId,
|
parentId: parentDeviceId,
|
||||||
name: '',
|
name: '',
|
||||||
@@ -184,15 +184,17 @@ export const normalizeEquipmentDetail = (
|
|||||||
context: LedgerContextIds = { engineeringId: '', projectId: '', deviceId: '' }
|
context: LedgerContextIds = { engineeringId: '', projectId: '', deviceId: '' }
|
||||||
): AddLedger.EquipmentForm => {
|
): AddLedger.EquipmentForm => {
|
||||||
const data = (detail || {}) as Record<string, unknown>
|
const data = (detail || {}) as Record<string, unknown>
|
||||||
|
const equipmentName = resolveString(data, 'name', 'dev_name', 'devName', 'deviceName') || node?.name || ''
|
||||||
|
const networkParam = resolveString(data, 'mac', 'ndid', 'unnid')
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: resolveString(data, 'id', 'equipmentId') || node?.id || '',
|
id: resolveString(data, 'id', 'equipmentId') || node?.id || '',
|
||||||
engineeringId: resolveString(data, 'engineeringId', 'associated_engineering') || context.engineeringId,
|
engineeringId: resolveString(data, 'engineeringId', 'associated_engineering') || context.engineeringId,
|
||||||
projectId: resolveString(data, 'projectId', 'associated_project') || context.projectId,
|
projectId: resolveString(data, 'projectId', 'associated_project') || context.projectId,
|
||||||
parentId: context.projectId,
|
parentId: context.projectId,
|
||||||
name: resolveString(data, 'name') || node?.name || '',
|
name: equipmentName,
|
||||||
ndid: resolveString(data, 'ndid'),
|
ndid: networkParam,
|
||||||
mac: resolveString(data, 'mac'),
|
mac: networkParam,
|
||||||
dev_type: resolveString(data, 'dev_type', 'devType'),
|
dev_type: resolveString(data, 'dev_type', 'devType'),
|
||||||
dev_model: resolveString(data, 'dev_model', 'devModel'),
|
dev_model: resolveString(data, 'dev_model', 'devModel'),
|
||||||
dev_access_method: resolveString(data, 'dev_access_method', 'devAccessMethod'),
|
dev_access_method: resolveString(data, 'dev_access_method', 'devAccessMethod'),
|
||||||
|
|||||||
Reference in New Issue
Block a user