st-comp 0.0.57 → 0.0.58

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "st-comp",
3
3
  "public": true,
4
- "version": "0.0.57",
4
+ "version": "0.0.58",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "dev": "vite",
@@ -0,0 +1,418 @@
1
+ <!-- 常用指标组件 -->
2
+ <script setup name="CommonIndicator">
3
+ import { ref, watch, computed } from "vue";
4
+
5
+ const props = defineProps({
6
+ config: { type: Object, default: () => {} },
7
+ varietyMarket: { type: null || Number, default: () => null }, // 已选品种市场
8
+ commonOption: { type: Array, default: () => [] }, // 已选常用选项
9
+ });
10
+ const data = defineModel("data", { default: [] });
11
+
12
+ // 常用指标项(是否展示受到: 品种市场, 常用选项影响)
13
+ const commonIndicatorOptions = computed(() => {
14
+ let result = [];
15
+ // 如果品种市场和常用选项一个都没有选, 那么就展示全部的常用指标
16
+ if (props.varietyMarket === null && !props.commonOption.length) {
17
+ result = props.config.options;
18
+ } else {
19
+ result = props.config.options;
20
+ // 1. 如果选择了品种市场, 进行一波筛选
21
+ if (props.varietyMarket) {
22
+ result = result.filter(({ parent }) => parent.varietyMarketIds.includes(props.varietyMarket));
23
+ }
24
+ // 2. 如果选择了常用选项, 进行一波筛选
25
+ if (props.commonOption.length) {
26
+ result = result.filter(({ parent }) => {
27
+ const { commonOptionIds } = parent;
28
+ // 只要选择了的常用选项里面, 有commonOptionIds中的值, 那么就展示, 即判断两个数组是否有重复元素
29
+ return (
30
+ [...new Set([...commonOptionIds, ...props.commonOption])].length !==
31
+ [...commonOptionIds, ...props.commonOption].length
32
+ );
33
+ });
34
+ }
35
+ }
36
+ return result;
37
+ });
38
+ watch(
39
+ () => commonIndicatorOptions.value,
40
+ () => {
41
+ // 如果常用指标已经进行了勾选, 则只保留常用指标中存在的值
42
+ if (data.value.length) {
43
+ data.value = data.value.filter(({ key }) => {
44
+ return commonIndicatorOptions.value.find((item) => item.key === key);
45
+ });
46
+ }
47
+ }
48
+ );
49
+
50
+ const visible = ref(false);
51
+ // 当前点击的指标项type
52
+ const nowIndicator = ref({});
53
+ // 当前指标项内容中的值
54
+ const indicatorValue = ref({});
55
+
56
+ // 函数: 点击指标项 (新增custom配置项时, 需要改动)
57
+ const clickIndicator = (item) => {
58
+ // 存储目前点击的指标项相关配置
59
+ nowIndicator.value = item;
60
+ // 初始化值---------------------------------
61
+ const baseParams = {
62
+ label: item.label,
63
+ key: item.key,
64
+ type: item.type,
65
+ };
66
+ if (item.type === undefined) {
67
+ indicatorValue.value = {
68
+ ...baseParams,
69
+ range: [null, null],
70
+ unit: item.defaultUnit.length ? [...item.defaultUnit] : [null, null],
71
+ };
72
+ }
73
+ // 是否ST
74
+ else if (item.key === "st") {
75
+ indicatorValue.value = { ...baseParams, st: null };
76
+ }
77
+ // 净利润
78
+ else if (item.key === "tFeaturelncomes") {
79
+ indicatorValue.value = {
80
+ ...baseParams,
81
+ rule: 1, // 1超 2跌
82
+ withFewYears: null,
83
+ yearsCount: null,
84
+ netProfit: null,
85
+ };
86
+ }
87
+ // 认沽认购
88
+ else if (item.key === "optionsCpType") {
89
+ indicatorValue.value = {
90
+ ...baseParams,
91
+ optionsCpType: null, // 1看涨 2看跌
92
+ };
93
+ }
94
+ // -----------------------------------------
95
+ // 打开弹窗
96
+ visible.value = true;
97
+ };
98
+
99
+ // 函数: 点击快捷选项[type-undefined 通用性]
100
+ const clickConvenientOption = (item) => {
101
+ indicatorValue.value.range = [...item.range];
102
+ indicatorValue.value.unit = [...item.unit];
103
+ };
104
+ // 函数: 点击快捷选项[特殊-净利润]
105
+ const clicktFeaturelncomesOption = (value) => {
106
+ switch (value) {
107
+ case "≤0":
108
+ Object.assign(indicatorValue.value, {
109
+ rule: 2, // 1超 2跌
110
+ withFewYears: 1,
111
+ yearsCount: 1,
112
+ netProfit: 0,
113
+ });
114
+ break;
115
+ case "≥0":
116
+ Object.assign(indicatorValue.value, {
117
+ rule: 1,
118
+ withFewYears: 1,
119
+ yearsCount: 1,
120
+ netProfit: 0,
121
+ });
122
+ break;
123
+ case "五年内有四年以上盈利超过5千万":
124
+ Object.assign(indicatorValue.value, {
125
+ rule: 1,
126
+ withFewYears: 5,
127
+ yearsCount: 4,
128
+ netProfit: 0.5,
129
+ });
130
+ break;
131
+ case "≥10亿":
132
+ Object.assign(indicatorValue.value, {
133
+ rule: 1,
134
+ withFewYears: 1,
135
+ yearsCount: 1,
136
+ netProfit: 10,
137
+ });
138
+ break;
139
+ case "≥20亿":
140
+ Object.assign(indicatorValue.value, {
141
+ rule: 1,
142
+ withFewYears: 1,
143
+ yearsCount: 1,
144
+ netProfit: 20,
145
+ });
146
+ break;
147
+ }
148
+ };
149
+
150
+ // 函数: 确定 (新增custom配置项时, 需要改动)
151
+ const submitDialog = () => {
152
+ // 校验 + 格式化文案
153
+ if (indicatorValue.value.type === undefined) {
154
+ const { label, range, unit } = indicatorValue.value;
155
+ // 校验
156
+ if (["", null].includes(range[0]) && ["", null].includes(range[1])) {
157
+ return ElMessage.warning("格式错误: 请至少填写一个值");
158
+ }
159
+ // 格式化文案
160
+ if (!["", null].includes(range[0]) && !["", null].includes(range[1])) {
161
+ indicatorValue.value.tagText = `${label}: ${range[0]}${unit[0] ?? ""} ~ ${range[1]}${unit[1] ?? ""}`;
162
+ } else if (!["", null].includes(range[0])) {
163
+ indicatorValue.value.tagText = `${label}: ≥${range[0]}${unit[0] ?? ""}`;
164
+ } else {
165
+ indicatorValue.value.tagText = `${label}: ≤${range[1]}${unit[1] ?? ""}`;
166
+ }
167
+ } else if (indicatorValue.value.key === "st") {
168
+ const { label, st } = indicatorValue.value;
169
+ // 校验
170
+ if (st === null) return ElMessage.warning("格式错误: 请至少选择一个值");
171
+ // 格式化文案
172
+ indicatorValue.value.tagText = `${label}: ${st ? "是" : "否"}`;
173
+ } else if (indicatorValue.value.key === "tFeaturelncomes") {
174
+ const { label, rule, withFewYears, yearsCount, netProfit } = indicatorValue.value;
175
+ // 校验
176
+ if (["", null].includes(withFewYears) || ["", null].includes(yearsCount) || ["", null].includes(netProfit)) {
177
+ return ElMessage.warning("格式错误: 请填写完整");
178
+ }
179
+ // 格式化文案
180
+ indicatorValue.value.tagText = `${label}: ${withFewYears}年内${yearsCount}年以上盈利${
181
+ rule === 1 ? "超" : "跌"
182
+ }过${netProfit}亿`;
183
+ } else if (indicatorValue.value.key === "optionsCpType") {
184
+ const { label, optionsCpType } = indicatorValue.value;
185
+ // 校验
186
+ if (optionsCpType === null) return ElMessage.warning("格式错误: 请至少选择一个值");
187
+ // 格式化文案
188
+ indicatorValue.value.tagText = `${label}: ${optionsCpType ? "看涨" : "看跌"}`;
189
+ }
190
+ // 判断data中是否已存在同样key的数据, 如果存在则替换, 如果不存在就push
191
+ const keyIndex = data.value.findIndex(({ key }) => key === indicatorValue.value.key);
192
+ if (keyIndex === -1) {
193
+ data.value.push(indicatorValue.value);
194
+ } else {
195
+ data.value.splice(keyIndex, 1, indicatorValue.value);
196
+ }
197
+ visible.value = false;
198
+ };
199
+
200
+ // 函数: 删除指标项Tag
201
+ const deleteTag = (index) => {
202
+ data.value.splice(index, 1);
203
+ };
204
+ // 函数: 编辑指标项Tag
205
+ const editTag = (item) => {
206
+ nowIndicator.value = commonIndicatorOptions.value.find(({ key }) => key === item.key);
207
+ indicatorValue.value = item;
208
+ visible.value = true;
209
+ };
210
+ </script>
211
+
212
+ <template>
213
+ <div class="common-indicator" v-if="config.show && commonIndicatorOptions.length">
214
+ <!-- 指标项展示区域 -->
215
+ <div class="indicator">
216
+ <div class="title">
217
+ <span>常用指标: </span>
218
+ <span>不限</span>
219
+ </div>
220
+ <div class="options">
221
+ <span v-for="item in commonIndicatorOptions" :key="item.key" @click="clickIndicator(item)"> {{ item.label }} </span>
222
+ </div>
223
+ </div>
224
+ <div class="tags">
225
+ <el-tag v-for="(item, index) in data" closable type="info" @close="deleteTag(index)">
226
+ <span>{{ item.tagText }}</span>
227
+ <span class="edit" @click="editTag(item)">编辑</span>
228
+ </el-tag>
229
+ </div>
230
+ <!-- 指标具体弹窗 -->
231
+ <el-dialog v-model="visible" :title="nowIndicator.label" width="600" align-center destroy-on-close>
232
+ <!-- type: undefined 为默认便捷配置项 + 输入框形式 -->
233
+ <template v-if="nowIndicator.type === undefined">
234
+ <!-- 便捷配置项区域 -->
235
+ <div class="convenient-option-box" v-if="nowIndicator.convenientOptions.length">
236
+ <el-button
237
+ v-for="item in nowIndicator.convenientOptions"
238
+ :key="item.text"
239
+ @click="clickConvenientOption(item)"
240
+ >{{ item.text }}</el-button
241
+ >
242
+ </div>
243
+ <!-- 输入框区域 -->
244
+ <div class="out-box">
245
+ <span>自定义: </span>
246
+ <el-input v-model="indicatorValue.range[0]" style="flex: 1">
247
+ <!-- 单位 -->
248
+ <template #append v-if="nowIndicator.unitOptions.length">
249
+ <span v-if="nowIndicator.unitOptions.length === 1">{{ indicatorValue.unit[0] }}</span>
250
+ <el-select
251
+ v-else
252
+ v-model="indicatorValue.unit[0]"
253
+ style="width: 72px"
254
+ >
255
+ <el-option v-for="unit in nowIndicator.unitOptions" :label="unit" :value="unit" />
256
+ </el-select>
257
+ </template>
258
+ </el-input>
259
+ ~
260
+ <el-input v-model="indicatorValue.range[1]" style="flex: 1">
261
+ <!-- 单位 -->
262
+ <template #append v-if="nowIndicator.unitOptions.length">
263
+ <span v-if="nowIndicator.unitOptions.length === 1">{{ indicatorValue.unit[1] }}</span>
264
+ <el-select
265
+ v-else
266
+ v-model="indicatorValue.unit[1]"
267
+ style="width: 72px"
268
+ >
269
+ <el-option v-for="unit in nowIndicator.unitOptions" :label="unit" :value="unit" />
270
+ </el-select>
271
+ </template>
272
+ </el-input>
273
+ </div>
274
+ </template>
275
+ <!-- type: custom 根据不同key进行自定义 -->
276
+ <template v-else>
277
+ <!-- 是否ST -->
278
+ <div v-if="nowIndicator.key === 'st'">
279
+ <el-radio-group v-model="indicatorValue.st">
280
+ <el-radio :label="1">是</el-radio>
281
+ <el-radio :label="0">否</el-radio>
282
+ </el-radio-group>
283
+ </div>
284
+ <!-- 净利润 -->
285
+ <div v-if="nowIndicator.key === 'tFeaturelncomes'">
286
+ <!-- 便捷配置项区域 -->
287
+ <div class="tFeaturelncomes-option-box">
288
+ <el-button @click="clicktFeaturelncomesOption('≤0')">≤0</el-button>
289
+ <el-button @click="clicktFeaturelncomesOption('≥0')">≥0</el-button>
290
+ <el-button @click="clicktFeaturelncomesOption('五年内有四年以上盈利超过5千万')"
291
+ >五年内有四年以上盈利超过5千万</el-button
292
+ >
293
+ <el-button @click="clicktFeaturelncomesOption('≥10亿')">≥10亿</el-button>
294
+ <el-button @click="clicktFeaturelncomesOption('≥20亿')">≥20亿</el-button>
295
+ </div>
296
+ <!-- 输入框区域 -->
297
+ <div class="tFeaturelncomes-out-box">
298
+ <span>自定义: </span>
299
+ <div>
300
+ <el-input-number v-model="indicatorValue.withFewYears" controls-position="right" />
301
+ 年内
302
+ <el-input-number v-model="indicatorValue.yearsCount" controls-position="right" />
303
+ 年以上盈利{{ indicatorValue.rule === 1 ? "超" : "跌" }}过
304
+ <el-input-number v-model="indicatorValue.netProfit" controls-position="right" />
305
+ 亿
306
+ </div>
307
+ </div>
308
+ </div>
309
+ <!-- 认沽认购 -->
310
+ <div v-if="nowIndicator.key === 'optionsCpType'">
311
+ <el-radio-group v-model="indicatorValue.optionsCpType">
312
+ <el-radio :label="1">看涨</el-radio>
313
+ <el-radio :label="0">看跌</el-radio>
314
+ </el-radio-group>
315
+ </div>
316
+ </template>
317
+ <template #footer>
318
+ <div class="dialog-footer">
319
+ <el-button type="primary" @click="submitDialog">确定</el-button>
320
+ </div>
321
+ </template>
322
+ </el-dialog>
323
+ </div>
324
+ </template>
325
+
326
+ <style lang="scss" scoped>
327
+ .common-indicator {
328
+ .indicator {
329
+ display: flex;
330
+ align-items: start;
331
+ .title {
332
+ height: 24px;
333
+ min-width: 100px;
334
+ font-size: 12px;
335
+ line-height: 24px;
336
+ display: flex;
337
+ align-items: center;
338
+ span:nth-child(1) {
339
+ width: 60px;
340
+ }
341
+ span:nth-child(2) {
342
+ cursor: pointer;
343
+ color: var(--el-color-primary);
344
+ }
345
+ }
346
+ .options {
347
+ span {
348
+ font-size: 12px;
349
+ line-height: 24px;
350
+ color: var(--el-text-color-regular);
351
+ padding: 4px 0;
352
+ margin-right: 20px;
353
+ cursor: pointer;
354
+ &:hover{
355
+ color: var(--el-color-primary)
356
+ }
357
+ }
358
+ }
359
+ }
360
+ :deep(.el-button.is-text) {
361
+ span {
362
+ line-height: 24px;
363
+ color: var(--el-text-color-regular);
364
+ }
365
+ }
366
+ .tags {
367
+ .el-tag {
368
+ margin: 6px 10px 6px 0;
369
+ .edit {
370
+ cursor: pointer;
371
+ color: var(--el-color-primary);
372
+ margin-left: 6px;
373
+ }
374
+ }
375
+ }
376
+ .convenient-option-box {
377
+ display: grid;
378
+ grid-template-columns: 33.3% 33.3% 33.3%;
379
+ border-bottom: 1px solid var(--el-border-color);
380
+ margin-bottom: 10px;
381
+ .el-button {
382
+ margin-left: 10px;
383
+ margin-right: 10px;
384
+ margin-bottom: 10px;
385
+ }
386
+ }
387
+ .out-box {
388
+ display: flex;
389
+ align-items: center;
390
+ padding: 0 10px;
391
+ span {
392
+ margin-right: 10px;
393
+ }
394
+ }
395
+ .tFeaturelncomes-option-box {
396
+ display: grid;
397
+ grid-template-columns: 20% 20% 60%;
398
+ border-bottom: 1px solid var(--el-border-color);
399
+ margin-bottom: 10px;
400
+ .el-button {
401
+ margin-left: 10px;
402
+ margin-right: 10px;
403
+ margin-bottom: 10px;
404
+ }
405
+ }
406
+ .tFeaturelncomes-out-box {
407
+ display: flex;
408
+ align-items: center;
409
+ padding: 0 10px;
410
+ span {
411
+ margin-right: 10px;
412
+ }
413
+ .el-input-number {
414
+ width: 100px;
415
+ }
416
+ }
417
+ }
418
+ </style>
@@ -0,0 +1,151 @@
1
+ <!-- 因子筛选组件 -->
2
+ <script setup name="FactorScreen">
3
+ import { ref } from "vue";
4
+ import { Plus, CircleCloseFilled } from "@element-plus/icons-vue";
5
+
6
+ const props = defineProps({ config: { type: Object, default: () => {} } });
7
+ const data = defineModel("data", { default: [] }); // 父组件对应该组件字段的数据
8
+
9
+ // 弹窗开关
10
+ const visible = ref(false);
11
+
12
+ // 表单相关参数
13
+ const dialogFormRef = ref(null);
14
+ const dialogForm = ref({ factorScreen: [{ cycle: null, factor: null, score: [null, null] }] });
15
+ // 特殊校验: 分数
16
+ const scoreValidator = (rule, value, callback) => {
17
+ // 检查开始和结束的分是否都没填
18
+ if (!value[0] && value[0] !== 0 && !value[1] && value[1] !== 0) {
19
+ callback(new Error("至少填写一个分数"));
20
+ } else {
21
+ callback();
22
+ }
23
+ };
24
+
25
+ // 函数: 弹窗外-添加因子
26
+ const openDialog = () => {
27
+ // 弹窗打开时, 重置表单
28
+ if (data.value.length > 0) dialogForm.value.factorScreen = JSON.parse(JSON.stringify(data.value));
29
+ else dialogForm.value.factorScreen = [{ cycle: null, factor: null, score: [null, null] }];
30
+ visible.value = true;
31
+ };
32
+ // 函数: 弹窗外-删除因子
33
+ const deleteTag = (index) => {
34
+ data.value.splice(index, 1);
35
+ };
36
+
37
+ // 函数: 弹窗内-添加因子
38
+ const addFactor = () => {
39
+ dialogForm.value.factorScreen.push({ cycle: null, factor: null, score: [null, null] });
40
+ };
41
+ // 函数: 弹窗内-删除因子
42
+ const deleteFactor = (index) => {
43
+ dialogForm.value.factorScreen.splice(index, 1);
44
+ };
45
+ // 函数: 确定
46
+ const submit = () => {
47
+ if (!dialogFormRef.value) return;
48
+ dialogFormRef.value.validate((valid) => {
49
+ if (valid) {
50
+ data.value = JSON.parse(JSON.stringify(dialogForm.value.factorScreen));
51
+ visible.value = false;
52
+ } else {
53
+ console.error("因子筛选-弹窗: 表单校验未通过");
54
+ }
55
+ });
56
+ };
57
+
58
+ // 映射
59
+ const handleTagName = (tag) => {
60
+ const { cycle, factor, score } = tag;
61
+ let str = "";
62
+ str += ` ${props.config.cycleOptions.find(({ value }) => value === cycle).label}`;
63
+ str += ` ${props.config.factorOptions.find(({ value }) => value === factor).label}`;
64
+ str += ` ${score[0] || score[0] === 0 ? `${score[0]}分` : "∞"}`;
65
+ str += " ~";
66
+ str += ` ${score[1] || score[1] === 0 ? `${score[1]}分` : "∞"}`;
67
+ return str;
68
+ };
69
+ </script>
70
+
71
+ <template>
72
+ <div class="factor-screen">
73
+ <!-- Tag展示区域 -->
74
+ <template v-if="data.length">
75
+ <el-tag v-for="(tag, index) in data" closable type="info" @close="deleteTag(index)">
76
+ {{ handleTagName(tag) }}
77
+ </el-tag>
78
+ </template>
79
+ <!-- 添加按钮区域 -->
80
+ <el-button class="screen-btn" type="primary" plain size="small" :icon="Plus" @click="openDialog"
81
+ >添加因子</el-button
82
+ >
83
+ <!-- Tag弹窗 -->
84
+ <el-dialog v-model="visible" title="因子筛选" width="500" align-center destroy-on-close>
85
+ <el-form ref="dialogFormRef" :model="dialogForm">
86
+ <div v-for="(item, index) in dialogForm.factorScreen" class="form-row">
87
+ <!-- 周期 -->
88
+ <el-form-item
89
+ :prop="'factorScreen.' + index + '.cycle'"
90
+ :rules="{ required: true, message: '周期不能为空', trigger: 'blur' }"
91
+ style="width: 100px"
92
+ >
93
+ <el-select v-model="item.cycle" placeholder="选择周期" size="small">
94
+ <el-option v-for="{ label, value } in config.cycleOptions" :label="label" :value="value" :key="value" />
95
+ </el-select>
96
+ </el-form-item>
97
+ <!-- 因子 -->
98
+ <el-form-item
99
+ :prop="'factorScreen.' + index + '.factor'"
100
+ :rules="{ required: true, message: '因子不能为空', trigger: 'blur' }"
101
+ style="width: 100px"
102
+ >
103
+ <el-select v-model="item.factor" placeholder="选择因子" size="small">
104
+ <el-option v-for="{ label, value } in config.factorOptions" :label="label" :value="value" :key="value" />
105
+ </el-select>
106
+ </el-form-item>
107
+ <!-- 分数 -->
108
+ <el-form-item
109
+ :prop="'factorScreen.' + index + '.score'"
110
+ :rules="{ validator: scoreValidator, trigger: 'blur' }"
111
+ style="width: 200px"
112
+ >
113
+ <div style="display: flex; align-items: center; width: 100%; height: 24px">
114
+ <el-input-number v-model="item.score[0]" size="small" controls-position="right" />
115
+ <span>~</span>
116
+ <el-input-number v-model="item.score[1]" size="small" controls-position="right" />
117
+ </div>
118
+ </el-form-item>
119
+ <!-- 删除 -->
120
+ <el-icon @click="deleteFactor(index)"><CircleCloseFilled /></el-icon>
121
+ </div>
122
+ <el-button type="primary" plain size="small" :icon="Plus" @click="addFactor">添加因子</el-button>
123
+ </el-form>
124
+ <template #footer>
125
+ <div class="dialog-footer">
126
+ <el-button type="primary" @click="submit">确定</el-button>
127
+ </div>
128
+ </template>
129
+ </el-dialog>
130
+ </div>
131
+ </template>
132
+
133
+ <style lang="scss" scoped>
134
+ .factor-screen {
135
+ .el-tag {
136
+ margin-right: 10px;
137
+ }
138
+ .form-row {
139
+ display: flex;
140
+ align-items: center;
141
+ justify-content: space-between;
142
+ margin-bottom: 18px;
143
+ .el-form-item {
144
+ margin: 0;
145
+ }
146
+ .el-icon {
147
+ cursor: pointer;
148
+ }
149
+ }
150
+ }
151
+ </style>