vue-index.html 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295
  1. <!DOCTYPE html>
  2. <html lang="zh-CN">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <title>供应商交货计划分析 (Vue版)</title>
  7. <link rel="stylesheet" href="styles.css">
  8. <!-- 引入Vue 3 -->
  9. <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
  10. <!-- 引入SheetJS用于Excel解析 -->
  11. <script src="https://cdn.jsdelivr.net/npm/xlsx@0.18.5/dist/xlsx.full.min.js"></script>
  12. </head>
  13. <body>
  14. <div id="app">
  15. <div class="container">
  16. <h1>供应商交货计划分析</h1>
  17. <div class="upload-section">
  18. <label for="file-upload" class="upload-btn">
  19. 选择Excel文件
  20. <input
  21. type="file"
  22. id="file-upload"
  23. accept=".xlsx, .xls"
  24. style="display: none;"
  25. @change="handleFileUpload">
  26. </label>
  27. <span id="file-name" class="file-name">{{ fileName || '未选择文件' }}</span>
  28. <button
  29. id="analyze-btn"
  30. class="analyze-btn"
  31. :disabled="!fileName || isAnalyzing"
  32. @click="analyzeFile"
  33. >
  34. {{ isAnalyzing ? '分析中...' : '开始分析' }}
  35. </button>
  36. </div>
  37. <!-- 分析结果区域 -->
  38. <div class="analysis-section" v-if="fieldStats.length > 0">
  39. <h2>字段分析结果</h2>
  40. <div class="field-analysis">
  41. <div class="field-card" v-for="field in fieldStats" :key="field.fieldName">
  42. <h3>{{ field.fieldName }}</h3>
  43. <div class="field-details">
  44. <div class="field-detail">数据类型: {{ field.dataType }}</div>
  45. <div class="field-detail">总记录数: {{ field.totalCount }}</div>
  46. <div class="field-detail">非空值数: {{ field.nonEmptyCount }} ({{ field.nonEmptyPercentage.toFixed(2) }}%)</div>
  47. <div class="field-detail">空值数: {{ field.emptyCount }} ({{ field.emptyPercentage.toFixed(2) }}%)</div>
  48. <div class="field-detail">唯一值数量: {{ field.uniqueValues }}</div>
  49. <!-- 数值类型的统计信息 -->
  50. <div v-if="field.min !== undefined" class="field-detail">最小值: {{ field.min }}</div>
  51. <div v-if="field.max !== undefined" class="field-detail">最大值: {{ field.max }}</div>
  52. <div v-if="field.average !== undefined" class="field-detail">平均值: {{ field.average.toFixed(2) }}</div>
  53. <!-- 样本值 -->
  54. <div v-if="field.sampleValues && field.sampleValues.length > 0" class="field-detail">
  55. 样本值: {{ field.sampleValues.join(', ') }}
  56. </div>
  57. </div>
  58. </div>
  59. </div>
  60. </div>
  61. <!-- 数据预览区域 -->
  62. <div class="preview-section" v-if="dataPreview.length > 0">
  63. <h2>数据预览</h2>
  64. <div class="data-preview">
  65. <table class="data-table">
  66. <thead>
  67. <tr>
  68. <th v-for="header in headers" :key="header">{{ header }}</th>
  69. </tr>
  70. </thead>
  71. <tbody>
  72. <tr v-for="(row, index) in dataPreview" :key="index">
  73. <td v-for="header in headers" :key="header">{{ row[header] || '' }}</td>
  74. </tr>
  75. </tbody>
  76. </table>
  77. <p v-if="totalRows > dataPreview.length" class="placeholder">
  78. 仅显示前{{ dataPreview.length }}行数据,共 {{ totalRows }} 行
  79. </p>
  80. </div>
  81. </div>
  82. <!-- 初始提示 -->
  83. <div v-if="!fileName && fieldStats.length === 0" class="placeholder">
  84. <p>请上传Excel文件以分析供应商交货计划数据</p>
  85. </div>
  86. </div>
  87. </div>
  88. <script>
  89. const { createApp, ref, reactive, computed } = Vue;
  90. createApp({
  91. setup() {
  92. // 响应式数据
  93. const fileName = ref('');
  94. const selectedFile = ref(null);
  95. const rawData = ref([]);
  96. const fieldStats = ref([]);
  97. const dataPreview = ref([]);
  98. const headers = ref([]);
  99. const totalRows = ref(0);
  100. const isAnalyzing = ref(false);
  101. // 特定日期字段列表
  102. const DATE_FIELDS = ['交货日期', '交期回复'];
  103. // 处理文件上传
  104. const handleFileUpload = (event) => {
  105. const file = event.target.files[0];
  106. if (file) {
  107. fileName.value = file.name;
  108. selectedFile.value = file;
  109. // 重置之前的分析结果
  110. fieldStats.value = [];
  111. dataPreview.value = [];
  112. rawData.value = [];
  113. headers.value = [];
  114. totalRows.value = 0;
  115. }
  116. };
  117. // 分析文件
  118. const analyzeFile = () => {
  119. if (selectedFile.value) {
  120. isAnalyzing.value = true;
  121. parseExcelFile(selectedFile.value);
  122. }
  123. };
  124. // 解析Excel文件
  125. const parseExcelFile = (file) => {
  126. const reader = new FileReader();
  127. reader.onload = function(e) {
  128. const data = new Uint8Array(e.target.result);
  129. const workbook = XLSX.read(data, { type: 'array' });
  130. // 获取第一个工作表
  131. const firstSheetName = workbook.SheetNames[0];
  132. const worksheet = workbook.Sheets[firstSheetName];
  133. // 转换为JSON格式
  134. rawData.value = XLSX.utils.sheet_to_json(worksheet);
  135. totalRows.value = rawData.value.length;
  136. // 分析字段
  137. analyzeFields();
  138. // 显示数据预览
  139. showDataPreview();
  140. // 分析完成,更新状态
  141. isAnalyzing.value = false;
  142. };
  143. reader.readAsArrayBuffer(file);
  144. };
  145. // 获取数据类型
  146. const getDataType = (value, fieldName = '') => {
  147. if (value === null || value === undefined) return 'unknown';
  148. // 对于特定的日期字段,强制识别为日期类型
  149. if (DATE_FIELDS.includes(fieldName)) {
  150. // 尝试各种日期格式解析
  151. const dateFormats = [
  152. value, // 原值
  153. value.replace(/\//g, '-'), // 将/替换为-(例如2023/05/20 -> 2023-05-20)
  154. value.replace(/\./g, '-') // 将.替换为-(例如2023.05.20 -> 2023-05-20)
  155. ];
  156. for (const format of dateFormats) {
  157. const date = new Date(format);
  158. if (!isNaN(date.getTime()) && date.toISOString() !== '0001-01-01T00:00:00.000Z' && date.getFullYear() > 1900) {
  159. return 'date';
  160. }
  161. }
  162. }
  163. // 尝试检查是否为日期
  164. if (typeof value === 'string') {
  165. const date = new Date(value);
  166. if (!isNaN(date.getTime()) && date.toISOString() !== '0001-01-01T00:00:00.000Z' && date.getFullYear() > 1900) {
  167. return 'date';
  168. }
  169. }
  170. // 检查是否为数字
  171. if (typeof value === 'number' || !isNaN(parseFloat(value)) && isFinite(value)) {
  172. return 'number';
  173. }
  174. // 检查是否为布尔值
  175. if (typeof value === 'boolean' || value === 'true' || value === 'false') {
  176. return 'boolean';
  177. }
  178. return 'string';
  179. };
  180. // 分析单个字段的统计信息
  181. const analyzeField = (fieldName) => {
  182. const values = rawData.value
  183. .map(row => row[fieldName])
  184. .filter(val => val !== undefined && val !== null && val !== '');
  185. const stats = {
  186. fieldName,
  187. totalCount: rawData.value.length,
  188. nonEmptyCount: values.length,
  189. emptyCount: rawData.value.length - values.length,
  190. dataType: values.length > 0 ? getDataType(values[0], fieldName) : 'unknown',
  191. uniqueValues: [...new Set(values)].length,
  192. sampleValues: values.slice(0, 5), // 显示前5个样本值
  193. nonEmptyPercentage: 0,
  194. emptyPercentage: 0
  195. };
  196. // 计算百分比
  197. stats.nonEmptyPercentage = (stats.nonEmptyCount / stats.totalCount) * 100;
  198. stats.emptyPercentage = (stats.emptyCount / stats.totalCount) * 100;
  199. // 如果是数值类型,计算额外的统计信息
  200. if (stats.dataType === 'number' || stats.dataType === 'date') {
  201. const numericValues = values.map(val => {
  202. if (stats.dataType === 'date') {
  203. // 处理日期类型
  204. return new Date(val).getTime();
  205. }
  206. return parseFloat(val);
  207. }).filter(val => !isNaN(val));
  208. if (numericValues.length > 0) {
  209. stats.min = Math.min(...numericValues);
  210. stats.max = Math.max(...numericValues);
  211. stats.average = numericValues.reduce((sum, val) => sum + val, 0) / numericValues.length;
  212. // 如果是日期类型,格式化最小和最大值
  213. if (stats.dataType === 'date') {
  214. stats.min = new Date(stats.min).toLocaleDateString();
  215. stats.max = new Date(stats.max).toLocaleDateString();
  216. }
  217. }
  218. }
  219. return stats;
  220. };
  221. // 分析所有字段
  222. const analyzeFields = () => {
  223. if (!rawData.value || rawData.value.length === 0) {
  224. fieldStats.value = [];
  225. return;
  226. }
  227. // 获取所有字段名
  228. headers.value = Object.keys(rawData.value[0]);
  229. // 分析每个字段
  230. fieldStats.value = headers.value.map(field => analyzeField(field));
  231. };
  232. // 显示数据预览
  233. const showDataPreview = () => {
  234. if (!rawData.value || rawData.value.length === 0) {
  235. dataPreview.value = [];
  236. return;
  237. }
  238. // 只显示前10行数据
  239. dataPreview.value = rawData.value.slice(0, 10);
  240. };
  241. return {
  242. fileName,
  243. selectedFile,
  244. fieldStats,
  245. dataPreview,
  246. headers,
  247. totalRows,
  248. isAnalyzing,
  249. handleFileUpload,
  250. analyzeFile
  251. };
  252. }
  253. }).mount('#app');
  254. </script>
  255. </body>
  256. </html>