py-test-components 1.0.0

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.
@@ -0,0 +1,387 @@
1
+ <template>
2
+ <div class="py-weather">
3
+ <el-card class="weather-card" :body-style="{ padding: '0px' }">
4
+ <!-- 加载状态 -->
5
+ <div v-if="loading" class="loading-wrapper">
6
+ <i class="el-icon-loading"></i>
7
+ <span>加载天气数据...</span>
8
+ </div>
9
+
10
+ <!-- 错误状态 -->
11
+ <div v-else-if="error" class="error-wrapper">
12
+ <i class="el-icon-warning-outline"></i>
13
+ <span>{{ error }}</span>
14
+ <el-button type="text" @click="loadWeather">重试</el-button>
15
+ </div>
16
+
17
+ <!-- 天气内容 -->
18
+ <div v-else class="weather-content">
19
+ <!-- 头部:城市和日期 -->
20
+ <div class="weather-header">
21
+ <div class="location">
22
+ <i class="el-icon-location"></i>
23
+ <span class="city">{{ displayCity }}</span>
24
+ </div>
25
+ <div class="date">{{ currentDate }}</div>
26
+ </div>
27
+
28
+ <!-- 主体:温度和天气 -->
29
+ <div class="weather-main">
30
+ <div class="temperature">
31
+ <span class="temp-value">{{ weatherData.temperature }}</span>
32
+ <span class="temp-unit">°C</span>
33
+ </div>
34
+ <div class="weather-desc">
35
+ <i :class="weatherIcon"></i>
36
+ <span>{{ weatherData.description }}</span>
37
+ </div>
38
+ </div>
39
+
40
+ <!-- 详情:湿度、风速等 -->
41
+ <div class="weather-details">
42
+ <div class="detail-item">
43
+ <i class="el-icon-moisture"></i>
44
+ <span class="label">湿度</span>
45
+ <span class="value">{{ weatherData.humidity }}%</span>
46
+ </div>
47
+ <div class="detail-item">
48
+ <i class="el-icon-wind-power"></i>
49
+ <span class="label">风速</span>
50
+ <span class="value">{{ weatherData.windSpeed }} km/h</span>
51
+ </div>
52
+ <div class="detail-item">
53
+ <i class="el-icon-view"></i>
54
+ <span class="label">能见度</span>
55
+ <span class="value">{{ weatherData.visibility }} km</span>
56
+ </div>
57
+ </div>
58
+
59
+ <!-- 预报 -->
60
+ <div v-if="config.showForecast && forecast.length > 0" class="weather-forecast">
61
+ <div class="forecast-title">未来预报</div>
62
+ <div class="forecast-list">
63
+ <div
64
+ v-for="(item, index) in forecast"
65
+ :key="index"
66
+ class="forecast-item"
67
+ >
68
+ <span class="day">{{ item.day }}</span>
69
+ <i :class="getWeatherIcon(item.weather)"></i>
70
+ <span class="temp">{{ item.low }}° - {{ item.high }}°</span>
71
+ </div>
72
+ </div>
73
+ </div>
74
+ </div>
75
+ </el-card>
76
+ </div>
77
+ </template>
78
+
79
+ <script>
80
+ import store from '../store';
81
+ import { weatherApi } from '../utils/api';
82
+
83
+ export default {
84
+ name: 'PyWeather',
85
+
86
+ props: {
87
+ propData: {
88
+ default: null
89
+ }
90
+ },
91
+
92
+ data() {
93
+ return {
94
+ loading: false,
95
+ error: '',
96
+ weatherData: {
97
+ temperature: '--',
98
+ description: '--',
99
+ humidity: '--',
100
+ windSpeed: '--',
101
+ visibility: '--'
102
+ },
103
+ forecast: []
104
+ };
105
+ },
106
+
107
+ computed: {
108
+ config() {
109
+ return this.propData || {};
110
+ },
111
+
112
+ displayCity() {
113
+ return this.config.city || this.weatherData.city || '北京';
114
+ },
115
+
116
+ currentDate() {
117
+ const date = new Date();
118
+ const options = {
119
+ year: 'numeric',
120
+ month: 'long',
121
+ day: 'numeric',
122
+ weekday: 'long'
123
+ };
124
+ return date.toLocaleDateString('zh-CN', options);
125
+ },
126
+
127
+ weatherIcon() {
128
+ const iconMap = {
129
+ '晴': 'el-icon-sunny',
130
+ '多云': 'el-icon-cloudy-and-sunny',
131
+ '阴': 'el-icon-cloudy',
132
+ '雨': 'el-icon-heavy-rain',
133
+ '雪': 'el-icon-light-snow',
134
+ '雾': 'el-icon-foggy',
135
+ '霾': 'el-icon-foggy'
136
+ };
137
+ return iconMap[this.weatherData.description] || 'el-icon-sunny';
138
+ }
139
+ },
140
+
141
+ mounted() {
142
+ this.loadWeather();
143
+ },
144
+
145
+ watch: {
146
+ 'config.city': {
147
+ handler() {
148
+ this.loadWeather();
149
+ }
150
+ }
151
+ },
152
+
153
+ methods: {
154
+ // 加载天气数据
155
+ async loadWeather() {
156
+ // 如果有传入的模拟数据,直接使用
157
+ if (this.config.mockData) {
158
+ this.weatherData = { ...this.config.mockData };
159
+ if (this.config.forecast) {
160
+ this.forecast = this.config.forecast;
161
+ }
162
+ return;
163
+ }
164
+
165
+ try {
166
+ this.loading = true;
167
+ this.error = '';
168
+
169
+ const city = this.config.city || '北京';
170
+
171
+ // 获取当前天气
172
+ const currentRes = await weatherApi.getWeather(city);
173
+ this.weatherData = {
174
+ city: city,
175
+ temperature: currentRes.temperature || 25,
176
+ description: currentRes.weather || '晴',
177
+ humidity: currentRes.humidity || 60,
178
+ windSpeed: currentRes.windSpeed || 10,
179
+ visibility: currentRes.visibility || 10,
180
+ ...currentRes
181
+ };
182
+
183
+ // 获取预报
184
+ if (this.config.showForecast) {
185
+ const forecastRes = await weatherApi.getForecast(city);
186
+ this.forecast = forecastRes.data || this.getDefaultForecast();
187
+ }
188
+
189
+ // 触发加载完成事件
190
+ this.$emit('load', this.weatherData);
191
+ } catch (err) {
192
+ console.error('[PyWeather] 加载天气失败:', err);
193
+ this.error = '获取天气数据失败';
194
+ this.$emit('error', err);
195
+ } finally {
196
+ this.loading = false;
197
+ }
198
+ },
199
+
200
+ // 获取默认预报数据
201
+ getDefaultForecast() {
202
+ const days = ['明天', '后天', '周三', '周四', '周五'];
203
+ return days.map(day => ({
204
+ day,
205
+ weather: '晴',
206
+ high: 28,
207
+ low: 18
208
+ }));
209
+ },
210
+
211
+ // 获取天气图标
212
+ getWeatherIcon(weather) {
213
+ const iconMap = {
214
+ '晴': 'el-icon-sunny',
215
+ '多云': 'el-icon-cloudy-and-sunny',
216
+ '阴': 'el-icon-cloudy',
217
+ '雨': 'el-icon-heavy-rain',
218
+ '雪': 'el-icon-light-snow'
219
+ };
220
+ return iconMap[weather] || 'el-icon-sunny';
221
+ }
222
+ }
223
+ };
224
+ </script>
225
+
226
+ <style scoped>
227
+ .py-weather {
228
+ width: 100%;
229
+ max-width: 400px;
230
+ }
231
+
232
+ .weather-card {
233
+ border-radius: 12px;
234
+ overflow: hidden;
235
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
236
+ }
237
+
238
+ .loading-wrapper,
239
+ .error-wrapper {
240
+ padding: 40px;
241
+ text-align: center;
242
+ color: #909399;
243
+ }
244
+
245
+ .error-wrapper i {
246
+ font-size: 48px;
247
+ color: #F56C6C;
248
+ display: block;
249
+ margin-bottom: 16px;
250
+ }
251
+
252
+ .loading-wrapper i {
253
+ font-size: 32px;
254
+ margin-right: 8px;
255
+ }
256
+
257
+ .weather-content {
258
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
259
+ color: white;
260
+ padding: 24px;
261
+ }
262
+
263
+ .weather-header {
264
+ display: flex;
265
+ justify-content: space-between;
266
+ align-items: center;
267
+ margin-bottom: 24px;
268
+ }
269
+
270
+ .location {
271
+ display: flex;
272
+ align-items: center;
273
+ font-size: 18px;
274
+ }
275
+
276
+ .location i {
277
+ margin-right: 8px;
278
+ }
279
+
280
+ .city {
281
+ font-weight: 500;
282
+ }
283
+
284
+ .date {
285
+ font-size: 14px;
286
+ opacity: 0.9;
287
+ }
288
+
289
+ .weather-main {
290
+ text-align: center;
291
+ margin-bottom: 24px;
292
+ }
293
+
294
+ .temperature {
295
+ display: flex;
296
+ align-items: flex-start;
297
+ justify-content: center;
298
+ }
299
+
300
+ .temp-value {
301
+ font-size: 72px;
302
+ font-weight: 300;
303
+ line-height: 1;
304
+ }
305
+
306
+ .temp-unit {
307
+ font-size: 24px;
308
+ margin-top: 8px;
309
+ opacity: 0.8;
310
+ }
311
+
312
+ .weather-desc {
313
+ margin-top: 12px;
314
+ font-size: 18px;
315
+ opacity: 0.9;
316
+ }
317
+
318
+ .weather-desc i {
319
+ font-size: 24px;
320
+ margin-right: 8px;
321
+ }
322
+
323
+ .weather-details {
324
+ display: flex;
325
+ justify-content: space-around;
326
+ padding: 16px 0;
327
+ border-top: 1px solid rgba(255, 255, 255, 0.2);
328
+ border-bottom: 1px solid rgba(255, 255, 255, 0.2);
329
+ }
330
+
331
+ .detail-item {
332
+ display: flex;
333
+ flex-direction: column;
334
+ align-items: center;
335
+ gap: 4px;
336
+ }
337
+
338
+ .detail-item i {
339
+ font-size: 20px;
340
+ opacity: 0.8;
341
+ }
342
+
343
+ .detail-item .label {
344
+ font-size: 12px;
345
+ opacity: 0.7;
346
+ }
347
+
348
+ .detail-item .value {
349
+ font-size: 14px;
350
+ font-weight: 500;
351
+ }
352
+
353
+ .weather-forecast {
354
+ margin-top: 16px;
355
+ }
356
+
357
+ .forecast-title {
358
+ font-size: 14px;
359
+ margin-bottom: 12px;
360
+ opacity: 0.9;
361
+ }
362
+
363
+ .forecast-list {
364
+ display: flex;
365
+ justify-content: space-between;
366
+ }
367
+
368
+ .forecast-item {
369
+ display: flex;
370
+ flex-direction: column;
371
+ align-items: center;
372
+ gap: 4px;
373
+ font-size: 12px;
374
+ }
375
+
376
+ .forecast-item .day {
377
+ opacity: 0.8;
378
+ }
379
+
380
+ .forecast-item i {
381
+ font-size: 20px;
382
+ }
383
+
384
+ .forecast-item .temp {
385
+ opacity: 0.9;
386
+ }
387
+ </style>
package/src/index.js ADDED
@@ -0,0 +1,51 @@
1
+ import Vue from 'vue';
2
+ import VueCustomElement from 'vue-custom-element';
3
+ import ElementUI from 'element-ui';
4
+ import 'element-ui/lib/theme-chalk/index.css';
5
+
6
+ // 引入组件
7
+ import PyTable from './components/PyTable.vue';
8
+ import PyWeather from './components/PyWeather.vue';
9
+
10
+ // 引入 store
11
+ import store from './store';
12
+
13
+ // 使用插件
14
+ Vue.use(ElementUI);
15
+ Vue.use(VueCustomElement);
16
+
17
+ // 注册 Web Components(必须禁用 Shadow DOM)
18
+ Vue.customElement('py-table', PyTable, {
19
+ shadow: false
20
+ });
21
+
22
+ Vue.customElement('py-weather', PyWeather, {
23
+ shadow: false
24
+ });
25
+
26
+ // initStore 函数 - 外部唯一入口
27
+ function initStore(config) {
28
+ if (!config || typeof config !== 'object') {
29
+ console.warn('[PyComponent] initStore 需要传入配置对象');
30
+ return;
31
+ }
32
+
33
+ // 将配置设置到 store 中
34
+ Object.keys(config).forEach(key => {
35
+ store.set(key, config[key]);
36
+ });
37
+
38
+ console.log('[PyComponent] Store 已初始化');
39
+ }
40
+
41
+ // 挂载到 window(浏览器环境)
42
+ if (typeof window !== 'undefined') {
43
+ window.PyComponent = {
44
+ initStore
45
+ // 注意:不暴露 store 实例
46
+ };
47
+ }
48
+
49
+ // 默认导出
50
+ export { initStore };
51
+ export default { initStore };
@@ -0,0 +1,156 @@
1
+ import React, { forwardRef, useEffect, useRef, useState } from 'react';
2
+
3
+ // ElementUI CSS URL
4
+ const ELEMENT_UI_CSS = 'https://unpkg.com/element-ui/lib/theme-chalk/index.css';
5
+
6
+ // 标记 CSS 是否已注入
7
+ let cssInjected = false;
8
+
9
+ /**
10
+ * 注入 ElementUI CSS
11
+ */
12
+ function injectElementUICSS() {
13
+ if (cssInjected || typeof document === 'undefined') return;
14
+
15
+ // 检查是否已存在
16
+ const existing = document.querySelector(`link[href="${ELEMENT_UI_CSS}"]`);
17
+ if (existing) {
18
+ cssInjected = true;
19
+ return;
20
+ }
21
+
22
+ const link = document.createElement('link');
23
+ link.rel = 'stylesheet';
24
+ link.href = ELEMENT_UI_CSS;
25
+ document.head.appendChild(link);
26
+ cssInjected = true;
27
+ }
28
+
29
+ /**
30
+ * 包装 Vue Web Component 为 React 组件
31
+ * @param {string} tagName - 自定义元素标签名
32
+ * @param {string} dataProp - 数据属性名
33
+ * @returns {React.Component} React 组件
34
+ */
35
+ function wrapVueComponent(tagName, dataProp = 'propData') {
36
+ return forwardRef(function WrappedComponent({
37
+ propData,
38
+ loading,
39
+ onChange,
40
+ onLoad,
41
+ onError,
42
+ onRefresh,
43
+ onSelectionChange,
44
+ onEdit,
45
+ onDelete,
46
+ onSizeChange,
47
+ onPageChange,
48
+ ...props
49
+ }, ref) {
50
+ const innerRef = useRef(null);
51
+ const [isReady, setIsReady] = useState(false);
52
+
53
+ // 合并 ref
54
+ useEffect(() => {
55
+ if (typeof ref === 'function') {
56
+ ref(innerRef.current);
57
+ } else if (ref) {
58
+ ref.current = innerRef.current;
59
+ }
60
+ }, [ref]);
61
+
62
+ // 加载组件库和 CSS
63
+ useEffect(() => {
64
+ injectElementUICSS();
65
+
66
+ // 动态加载组件库构建产物
67
+ import('../../dist/py-component.esm.js')
68
+ .then(() => {
69
+ setIsReady(true);
70
+ })
71
+ .catch(err => {
72
+ console.error(`[PyComponent] 加载失败:`, err);
73
+ });
74
+ }, []);
75
+
76
+ // 设置 propData 到 DOM property
77
+ useEffect(() => {
78
+ if (innerRef.current && isReady) {
79
+ innerRef.current[dataProp] = propData;
80
+ }
81
+ }, [propData, isReady, dataProp]);
82
+
83
+ // 绑定事件
84
+ useEffect(() => {
85
+ const el = innerRef.current;
86
+ if (!el || !isReady) return;
87
+
88
+ const events = {
89
+ 'change': onChange,
90
+ 'load': onLoad,
91
+ 'error': onError,
92
+ 'refresh': onRefresh,
93
+ 'selection-change': onSelectionChange,
94
+ 'edit': onEdit,
95
+ 'delete': onDelete,
96
+ 'size-change': onSizeChange,
97
+ 'page-change': onPageChange
98
+ };
99
+
100
+ Object.entries(events).forEach(([eventName, handler]) => {
101
+ if (handler) {
102
+ el.addEventListener(eventName, handler);
103
+ }
104
+ });
105
+
106
+ return () => {
107
+ Object.entries(events).forEach(([eventName, handler]) => {
108
+ if (handler) {
109
+ el.removeEventListener(eventName, handler);
110
+ }
111
+ });
112
+ };
113
+ }, [isReady, onChange, onLoad, onError, onRefresh, onSelectionChange, onEdit, onDelete, onSizeChange, onPageChange]);
114
+
115
+ // 显示 loading 状态
116
+ if (!isReady) {
117
+ return loading || <div style={{ padding: 20, textAlign: 'center' }}>加载中...</div>;
118
+ }
119
+
120
+ return React.createElement(tagName, {
121
+ ref: innerRef,
122
+ ...props
123
+ });
124
+ });
125
+ }
126
+
127
+ // 创建组件
128
+ export const PyTable = wrapVueComponent('py-table', 'propData');
129
+ export const PyWeather = wrapVueComponent('py-weather', 'propData');
130
+
131
+ /**
132
+ * 初始化 Store
133
+ * @param {object} config - 配置对象
134
+ * @returns {Promise<void>}
135
+ */
136
+ export function initStore(config) {
137
+ return new Promise((resolve, reject) => {
138
+ import('../../dist/py-component.esm.js')
139
+ .then((module) => {
140
+ const { initStore: init } = module;
141
+ if (init) {
142
+ init(config);
143
+ resolve();
144
+ } else {
145
+ reject(new Error('initStore 不存在'));
146
+ }
147
+ })
148
+ .catch(reject);
149
+ });
150
+ }
151
+
152
+ export default {
153
+ PyTable,
154
+ PyWeather,
155
+ initStore
156
+ };
@@ -0,0 +1,66 @@
1
+ import Vue from 'vue';
2
+
3
+ // 创建响应式 store 对象
4
+ const state = Vue.observable({
5
+ // 默认配置
6
+ apiKey: '',
7
+ baseUrl: '',
8
+ userInfo: null,
9
+ // 可扩展更多全局配置
10
+ });
11
+
12
+ // Store API
13
+ const store = {
14
+ /**
15
+ * 获取 store 中的值
16
+ * @param {string} key - 键名
17
+ * @returns {any} 值
18
+ */
19
+ get(key) {
20
+ return state[key];
21
+ },
22
+
23
+ /**
24
+ * 设置 store 中的值
25
+ * @param {string} key - 键名
26
+ * @param {any} value - 值
27
+ */
28
+ set(key, value) {
29
+ state[key] = value;
30
+ },
31
+
32
+ /**
33
+ * 批量设置值
34
+ * @param {object} config - 配置对象
35
+ */
36
+ setMultiple(config) {
37
+ Object.assign(state, config);
38
+ },
39
+
40
+ /**
41
+ * 订阅 store 变化(仅供内部使用)
42
+ * @param {function} callback - 回调函数
43
+ * @returns {function} 取消订阅函数
44
+ */
45
+ subscribe(callback) {
46
+ // 使用 Vue watch 实现订阅
47
+ const unwatch = Vue.watch(
48
+ () => state,
49
+ (newVal, oldVal) => {
50
+ callback(newVal, oldVal);
51
+ },
52
+ { deep: true }
53
+ );
54
+ return unwatch;
55
+ },
56
+
57
+ /**
58
+ * 获取整个 state(慎用,主要用于调试)
59
+ * @returns {object} state 对象
60
+ */
61
+ getState() {
62
+ return state;
63
+ }
64
+ };
65
+
66
+ export default store;