├── CTAI_flask ├── .vscode │ └── settings.json ├── app.py ├── core │ ├── __init__.py │ ├── __pycache__ │ │ ├── __init__.cpython-36.pyc │ │ ├── get_feature.cpython-36.pyc │ │ ├── main.cpython-36.pyc │ │ ├── predict.cpython-36.pyc │ │ └── process.cpython-36.pyc │ ├── get_feature.py │ ├── main.py │ ├── net │ │ └── inference_model │ │ │ ├── .success │ │ │ ├── __model__ │ │ │ ├── __params__ │ │ │ └── model.yml │ ├── predict.py │ └── process.py ├── data │ ├── N0039.png │ ├── P0134.png │ └── testfile.zip └── static │ └── index.html ├── CTAI_web ├── .gitignore ├── README.md ├── babel.config.js ├── package-lock.json ├── package.json ├── public │ ├── favicon.ico │ └── index.html ├── src │ ├── App.vue │ ├── assets │ │ └── style.css │ ├── components │ │ ├── Content.vue │ │ ├── Footer.vue │ │ └── Header.vue │ ├── main.js │ └── theme │ │ ├── fonts │ │ ├── element-icons.ttf │ │ └── element-icons.woff │ │ └── index.css ├── vue.config.js └── vue.md ├── LICENSE ├── README.md └── requirements.txt /CTAI_flask/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.pythonPath": "D:\\Anaconda3\\envs\\paddle_env\\python.exe" 3 | } -------------------------------------------------------------------------------- /CTAI_flask/app.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import logging as rel_log 3 | import os 4 | import shutil 5 | from datetime import timedelta 6 | from paddlex import deploy 7 | from flask import * 8 | 9 | import core.main 10 | 11 | UPLOAD_FOLDER = r'./uploads' 12 | 13 | ALLOWED_EXTENSIONS = set(['png']) 14 | app = Flask(__name__) 15 | app.secret_key = 'secret!' 16 | app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER 17 | 18 | werkzeug_logger = rel_log.getLogger('werkzeug') 19 | werkzeug_logger.setLevel(rel_log.ERROR) 20 | 21 | # 解决缓存刷新问题 22 | app.config['SEND_FILE_MAX_AGE_DEFAULT'] = timedelta(seconds=1) 23 | 24 | 25 | # 添加header解决跨域 26 | @app.after_request 27 | def after_request(response): 28 | response.headers['Access-Control-Allow-Origin'] = '*' 29 | response.headers['Access-Control-Allow-Credentials'] = 'true' 30 | response.headers['Access-Control-Allow-Methods'] = 'POST' 31 | response.headers['Access-Control-Allow-Headers'] = 'Content-Type, X-Requested-With' 32 | return response 33 | 34 | 35 | def allowed_file(filename): 36 | return '.' in filename and filename.rsplit('.', 1)[1] in ALLOWED_EXTENSIONS 37 | 38 | 39 | @app.route('/') 40 | def hello_world(): 41 | return redirect(url_for('static', filename='./index.html')) 42 | 43 | 44 | @app.route('/upload', methods=['GET', 'POST']) 45 | def upload_file(): 46 | file = request.files['file'] 47 | print(datetime.datetime.now(), file.filename) 48 | if file and allowed_file(file.filename): 49 | src_path = os.path.join(app.config['UPLOAD_FOLDER'], file.filename) 50 | file.save(src_path) 51 | shutil.copy(src_path, './tmp/ct') 52 | image_path = os.path.join('./tmp/ct', file.filename) 53 | print(src_path, image_path) 54 | pid, image_info = core.main.c_main(image_path, current_app.model) 55 | return jsonify({'status': 1, 56 | 'image_url': 'http://127.0.0.1:5003/tmp/ct/' + pid, 57 | 'draw_url': 'http://127.0.0.1:5003/tmp/draw/' + pid, 58 | 'image_info': image_info 59 | }) 60 | 61 | return jsonify({'status': 0}) 62 | 63 | 64 | @app.route("/download", methods=['GET']) 65 | def download_file(): 66 | # 需要知道2个参数, 第1个参数是本地目录的path, 第2个参数是文件名(带扩展名) 67 | return send_from_directory('data', 'testfile.zip', as_attachment=True) 68 | 69 | 70 | # show photo 71 | @app.route('/tmp/', methods=['GET']) 72 | def show_photo(file): 73 | if request.method == 'GET': 74 | if not file is None: 75 | image_data = open(f'tmp/{file}', "rb").read() 76 | response = make_response(image_data) 77 | response.headers['Content-Type'] = 'image/png' 78 | return response 79 | 80 | if __name__ == '__main__': 81 | files = [ 82 | 'uploads', 'tmp/ct', 'tmp/draw', 83 | 'tmp/image', 'tmp/mask', 'tmp/uploads' 84 | ] 85 | for ff in files: 86 | if not os.path.exists(ff): 87 | os.makedirs(ff) 88 | with app.app_context(): 89 | current_app.model = deploy.Predictor( 90 | './core/net/inference_model', use_gpu=True) 91 | app.run(host='127.0.0.1', port=5003, debug=True) 92 | -------------------------------------------------------------------------------- /CTAI_flask/core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sharpiless/PaddleX-Flask-VUE-demo/16ef48cea78e7f2d5c95279787c5103568aae3cd/CTAI_flask/core/__init__.py -------------------------------------------------------------------------------- /CTAI_flask/core/__pycache__/__init__.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sharpiless/PaddleX-Flask-VUE-demo/16ef48cea78e7f2d5c95279787c5103568aae3cd/CTAI_flask/core/__pycache__/__init__.cpython-36.pyc -------------------------------------------------------------------------------- /CTAI_flask/core/__pycache__/get_feature.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sharpiless/PaddleX-Flask-VUE-demo/16ef48cea78e7f2d5c95279787c5103568aae3cd/CTAI_flask/core/__pycache__/get_feature.cpython-36.pyc -------------------------------------------------------------------------------- /CTAI_flask/core/__pycache__/main.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sharpiless/PaddleX-Flask-VUE-demo/16ef48cea78e7f2d5c95279787c5103568aae3cd/CTAI_flask/core/__pycache__/main.cpython-36.pyc -------------------------------------------------------------------------------- /CTAI_flask/core/__pycache__/predict.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sharpiless/PaddleX-Flask-VUE-demo/16ef48cea78e7f2d5c95279787c5103568aae3cd/CTAI_flask/core/__pycache__/predict.cpython-36.pyc -------------------------------------------------------------------------------- /CTAI_flask/core/__pycache__/process.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sharpiless/PaddleX-Flask-VUE-demo/16ef48cea78e7f2d5c95279787c5103568aae3cd/CTAI_flask/core/__pycache__/process.cpython-36.pyc -------------------------------------------------------------------------------- /CTAI_flask/core/get_feature.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | from random import random 3 | 4 | import cv2 5 | import numpy as np 6 | import pandas as pd 7 | from numba import jit 8 | 9 | np.set_printoptions(suppress=True) # 输出时禁止科学表示法,直接输出小数值 10 | 11 | column_all_c = ['面积', '周长', '重心x', '重心y', '似圆度', '灰度均值', '灰度方差', '灰度偏度', 12 | '灰度峰态', '小梯度优势', '大梯度优势', '灰度分布不均匀性', '梯度分布不均匀性', '能量', '灰度平均', '梯度平均', 13 | '灰度均方差', '梯度均方差', '相关', '灰度熵', '梯度熵', '混合熵', '惯性', '逆差矩'] 14 | 15 | features_list = ['area', 'perimeter', 'focus_x', 'focus_y', 'ellipse', 'mean', 'std', 'piandu', 'fengdu', 16 | 'small_grads_dominance', 17 | 'big_grads_dominance', 'gray_asymmetry', 'grads_asymmetry', 'energy', 'gray_mean', 'grads_mean', 18 | 'gray_variance', 'grads_variance', 'corelation', 'gray_entropy', 'grads_entropy', 'entropy', 'inertia', 19 | 'differ_moment'] 20 | 21 | 22 | # 最后俩偏度 峰度 23 | 24 | 25 | # 获取变量的名 26 | def get_variable_name(variable): 27 | callers_local_vars = inspect.currentframe().f_back.f_locals.items() 28 | return [var_name for var_name, var_val in callers_local_vars if var_val is variable] 29 | 30 | 31 | def glcm(img_gray, ngrad=16, ngray=16): 32 | '''Gray Level-Gradient Co-occurrence Matrix,取归一化后的灰度值、梯度值分别为16、16''' 33 | # 利用sobel算子分别计算x-y方向上的梯度值 34 | gsx = cv2.Sobel(img_gray, cv2.CV_64F, 1, 0, ksize=3) 35 | gsy = cv2.Sobel(img_gray, cv2.CV_64F, 0, 1, ksize=3) 36 | height, width = img_gray.shape 37 | grad = (gsx ** 2 + gsy ** 2) ** 0.5 # 计算梯度值 38 | grad = np.asarray(1.0 * grad * (ngrad - 1) / grad.max(), dtype=np.int16) 39 | gray = np.asarray(1.0 * img_gray * (ngray - 1) / img_gray.max(), dtype=np.int16) # 0-255变换为0-15 40 | gray_grad = np.zeros([ngray, ngrad]) # 灰度梯度共生矩阵 41 | for i in range(height): 42 | for j in range(width): 43 | gray_value = gray[i][j] 44 | grad_value = grad[i][j] 45 | gray_grad[gray_value][grad_value] += 1 46 | gray_grad = 1.0 * gray_grad / (height * width) # 归一化灰度梯度矩阵,减少计算量 47 | get_glcm_features(gray_grad) 48 | 49 | 50 | @jit 51 | def get_gray_feature(): 52 | # 灰度特征提取算法 53 | hist = cv2.calcHist([image_ROI_uint8[index]], [0], None, [256], [0, 256]) 54 | # 假的 还没用灰度直方图 55 | 56 | c_features['mean'].append(np.mean(image_ROI[index])) 57 | c_features['std'].append(np.std(image_ROI[index])) 58 | 59 | s = pd.Series(image_ROI[index]) 60 | c_features['piandu'].append(s.skew()) 61 | c_features['fengdu'].append(s.kurt()) 62 | 63 | 64 | def get_glcm_features(mat): 65 | '''根据灰度梯度共生矩阵计算纹理特征量,包括小梯度优势,大梯度优势,灰度分布不均匀性,梯度分布不均匀性,能量,灰度平均,梯度平均, 66 | 灰度方差,梯度方差,相关,灰度熵,梯度熵,混合熵,惯性,逆差矩''' 67 | sum_mat = mat.sum() 68 | small_grads_dominance = big_grads_dominance = gray_asymmetry = grads_asymmetry = energy = gray_mean = grads_mean = 0 69 | gray_variance = grads_variance = corelation = gray_entropy = grads_entropy = entropy = inertia = differ_moment = 0 70 | for i in range(mat.shape[0]): 71 | gray_variance_temp = 0 72 | for j in range(mat.shape[1]): 73 | small_grads_dominance += mat[i][j] / ((j + 1) ** 2) 74 | big_grads_dominance += mat[i][j] * j ** 2 75 | energy += mat[i][j] ** 2 76 | if mat[i].sum() != 0: 77 | gray_entropy -= mat[i][j] * np.log(mat[i].sum()) 78 | if mat[:, j].sum() != 0: 79 | grads_entropy -= mat[i][j] * np.log(mat[:, j].sum()) 80 | if mat[i][j] != 0: 81 | entropy -= mat[i][j] * np.log(mat[i][j]) 82 | inertia += (i - j) ** 2 * np.log(mat[i][j]) 83 | differ_moment += mat[i][j] / (1 + (i - j) ** 2) 84 | gray_variance_temp += mat[i][j] ** 0.5 85 | 86 | gray_asymmetry += mat[i].sum() ** 2 87 | gray_mean += i * mat[i].sum() ** 2 88 | gray_variance += (i - gray_mean) ** 2 * gray_variance_temp 89 | for j in range(mat.shape[1]): 90 | grads_variance_temp = 0 91 | for i in range(mat.shape[0]): 92 | grads_variance_temp += mat[i][j] ** 0.5 93 | grads_asymmetry += mat[:, j].sum() ** 2 94 | grads_mean += j * mat[:, j].sum() ** 2 95 | grads_variance += (j - grads_mean) ** 2 * grads_variance_temp 96 | small_grads_dominance /= sum_mat 97 | big_grads_dominance /= sum_mat 98 | gray_asymmetry /= sum_mat 99 | grads_asymmetry /= sum_mat 100 | gray_variance = gray_variance ** 0.5 101 | grads_variance = grads_variance ** 0.5 102 | for i in range(mat.shape[0]): 103 | for j in range(mat.shape[1]): 104 | corelation += (i - gray_mean) * (j - grads_mean) * mat[i][j] 105 | glgcm_features = [small_grads_dominance, big_grads_dominance, gray_asymmetry, grads_asymmetry, energy, gray_mean, 106 | grads_mean, 107 | gray_variance, grads_variance, corelation, gray_entropy, grads_entropy, entropy, inertia, 108 | differ_moment] 109 | for i in range(len(glgcm_features)): 110 | t = get_variable_name(glgcm_features[i])[0] 111 | c_features[t].append(np.round(glgcm_features[i], 4)) 112 | 113 | 114 | def get_geometry_feature(): 115 | # 形态特征 分割mask获得一些特征 116 | contours, x = cv2.findContours(mask_array.copy(), cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE) 117 | tarea = [] 118 | tperimeter = [] 119 | for c in contours: 120 | # 生成矩 121 | try: 122 | M = cv2.moments(c) 123 | cx = int(M['m10'] / M['m00']) 124 | cy = int(M['m01'] / M['m00']) 125 | c_features['focus_x'].append(cx) 126 | c_features['focus_y'].append(cy) 127 | except: 128 | print('error') 129 | 130 | # 椭圆拟合 131 | try: 132 | (x, y), (MA, ma), angle = cv2.fitEllipse(c) 133 | c_features['ellipse'].append((ma - MA)) 134 | except: 135 | continue 136 | # 面积周长 137 | tarea.append(cv2.contourArea(c)) 138 | tperimeter.append(cv2.arcLength(c, True)) 139 | 140 | # 将mask里的最大值追加 有黑洞 141 | try: 142 | c_features['area'].append(max(tarea)) 143 | c_features['perimeter'].append(round(max(tperimeter), 4)) 144 | except: 145 | print('area error') 146 | 147 | 148 | # 提取肿瘤特征 149 | def get_feature(image, mask): 150 | global w 151 | global image_ROI_uint8, index, image_ROI_mini, image_ROI, mask_array 152 | 153 | mask_array = cv2.imread(mask, 0) 154 | image_arrary = cv2.imread(image) 155 | # 映射到CT获得特征 156 | image_ROI = np.zeros(shape=image_arrary.shape) 157 | index = np.nonzero(mask_array) 158 | if not index[0].any(): 159 | # c_features['no'] = True 160 | return None 161 | image_ROI[index] = image_arrary[index] 162 | image_ROI_uint8 = np.uint8(image_ROI) 163 | # 获得只有肿瘤的图片 164 | x, y, w, h = cv2.boundingRect(mask_array) 165 | image_ROI_mini = np.uint8(image_arrary[y:y + h, x:x + w]) 166 | w = image_ROI_mini 167 | 168 | # 灰度梯度共生矩阵提取纹理特征 169 | get_geometry_feature() 170 | # get_gray_feature() 171 | glcm(image_ROI_mini, 15, 15) 172 | 173 | return c_features 174 | 175 | 176 | def main(pid): 177 | global w 178 | 179 | person_id = pid 180 | global c_features 181 | c_features = {} 182 | for i in range(len(features_list)): 183 | c_features[features_list[i]] = [column_all_c[i], np.round(random(), 3)] 184 | 185 | return c_features 186 | 187 | 188 | if __name__ == '__main__': 189 | main() 190 | -------------------------------------------------------------------------------- /CTAI_flask/core/main.py: -------------------------------------------------------------------------------- 1 | from core import process, predict, get_feature 2 | 3 | 4 | def c_main(path, model): 5 | image_data = process.pre_process(path) 6 | predict.predict(image_data, model) 7 | process.last_process(image_data[1]) 8 | image_info = get_feature.main(image_data[1]) 9 | 10 | return image_data[1] + '.png', image_info 11 | 12 | 13 | if __name__ == '__main__': 14 | pass 15 | -------------------------------------------------------------------------------- /CTAI_flask/core/net/inference_model/.success: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sharpiless/PaddleX-Flask-VUE-demo/16ef48cea78e7f2d5c95279787c5103568aae3cd/CTAI_flask/core/net/inference_model/.success -------------------------------------------------------------------------------- /CTAI_flask/core/net/inference_model/__model__: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sharpiless/PaddleX-Flask-VUE-demo/16ef48cea78e7f2d5c95279787c5103568aae3cd/CTAI_flask/core/net/inference_model/__model__ -------------------------------------------------------------------------------- /CTAI_flask/core/net/inference_model/__params__: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sharpiless/PaddleX-Flask-VUE-demo/16ef48cea78e7f2d5c95279787c5103568aae3cd/CTAI_flask/core/net/inference_model/__params__ -------------------------------------------------------------------------------- /CTAI_flask/core/net/inference_model/model.yml: -------------------------------------------------------------------------------- 1 | Model: FastSCNN 2 | Transforms: 3 | - Resize: 4 | interp: LINEAR 5 | target_size: 512 6 | - Normalize: 7 | max_val: 8 | - 255.0 9 | - 255.0 10 | - 255.0 11 | mean: 12 | - 0.5 13 | - 0.5 14 | - 0.5 15 | min_val: 16 | - 0 17 | - 0 18 | - 0 19 | std: 20 | - 0.5 21 | - 0.5 22 | - 0.5 23 | - ResizeByShort: 24 | max_size: 512 25 | short_size: 512 26 | - Padding: 27 | im_padding_value: 28 | - 0.0 29 | - 0.0 30 | - 0.0 31 | label_padding_value: 255 32 | target_size: 33 | - 512 34 | - 512 35 | TransformsMode: BGR 36 | _Attributes: 37 | eval_metrics: 38 | miou: 0.8615462742519084 39 | fixed_input_shape: 40 | - 512 41 | - 512 42 | labels: 43 | - background 44 | - optic_disc 45 | model_type: segmenter 46 | num_classes: 2 47 | _ModelInputsOutputs: 48 | test_inputs: 49 | - - image 50 | - image 51 | test_outputs: 52 | - - pred 53 | - unsqueeze2_0.tmp_0 54 | - - logit 55 | - softmax_0.tmp_0 56 | _init_params: 57 | class_weight: null 58 | ignore_index: 255 59 | input_channel: 3 60 | multi_loss_weight: 61 | - 1.0 62 | num_classes: 2 63 | use_bce_loss: false 64 | use_dice_loss: false 65 | completed_epochs: 0 66 | status: Infer 67 | version: 1.3.4 68 | -------------------------------------------------------------------------------- /CTAI_flask/core/predict.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import cv2 3 | 4 | rate = 0.5 5 | 6 | def predict(dataset, model): 7 | global img_y 8 | x = dataset[0].replace('\\', '/') 9 | file_name = dataset[1] 10 | print(x) 11 | print(file_name) 12 | img_y = model.predict(x)['label_map'] 13 | img_y = img_y * 255 14 | img_y = img_y.astype(np.int) 15 | cv2.imwrite(f'./tmp/mask/{file_name}_mask.png', img_y, 16 | (cv2.IMWRITE_PNG_COMPRESSION, 0)) 17 | 18 | -------------------------------------------------------------------------------- /CTAI_flask/core/process.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import cv2 4 | 5 | 6 | def data_in_one(inputdata): 7 | if not inputdata.any(): 8 | return inputdata 9 | inputdata = (inputdata - inputdata.min()) / (inputdata.max() - inputdata.min()) 10 | return inputdata 11 | 12 | 13 | def pre_process(data_path): 14 | file_name = os.path.split(data_path)[1].split('.')[0] 15 | return data_path, file_name 16 | 17 | 18 | def last_process(file_name): 19 | image = cv2.imread(f'./tmp/ct/{file_name}.png') 20 | mask = cv2.imread(f'./tmp/mask/{file_name}_mask.png', 0) 21 | contours, hierarchy = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) 22 | cv2.drawContours(image, contours, -1, (0, 255, 0), 2) 23 | cv2.imwrite('./tmp/draw/{}.png'.format(file_name), image) 24 | -------------------------------------------------------------------------------- /CTAI_flask/data/N0039.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sharpiless/PaddleX-Flask-VUE-demo/16ef48cea78e7f2d5c95279787c5103568aae3cd/CTAI_flask/data/N0039.png -------------------------------------------------------------------------------- /CTAI_flask/data/P0134.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sharpiless/PaddleX-Flask-VUE-demo/16ef48cea78e7f2d5c95279787c5103568aae3cd/CTAI_flask/data/P0134.png -------------------------------------------------------------------------------- /CTAI_flask/data/testfile.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sharpiless/PaddleX-Flask-VUE-demo/16ef48cea78e7f2d5c95279787c5103568aae3cd/CTAI_flask/data/testfile.zip -------------------------------------------------------------------------------- /CTAI_flask/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Title 6 | 7 | 8 | 9 |
10 |

11 | 12 | 13 |

14 |
15 | 16 | 下载 17 | 18 | -------------------------------------------------------------------------------- /CTAI_web/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | 6 | # local env files 7 | .env.local 8 | .env.*.local 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | 16 | # Editor directories and files 17 | .idea 18 | .vscode 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | -------------------------------------------------------------------------------- /CTAI_web/README.md: -------------------------------------------------------------------------------- 1 | #serverless 2 | -------------------------------------------------------------------------------- /CTAI_web/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/app' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /CTAI_web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ct", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "lint": "vue-cli-service lint" 9 | }, 10 | "dependencies": { 11 | "axios": "^0.18.1", 12 | "echarts": "^4.3.0", 13 | "element-ui": "^2.12.0", 14 | "socket.io": "^2.2.0", 15 | "vue": "^2.6.10", 16 | "vue-codemirror": "^4.0.6", 17 | "vue-router": "^3.1.2", 18 | "vuex": "^3.1.1" 19 | }, 20 | "devDependencies": { 21 | "@vue/cli-plugin-babel": "^3.12.1", 22 | "@vue/cli-plugin-eslint": "^3.12.1", 23 | "@vue/cli-service": "^3.12.1", 24 | "babel-eslint": "^10.0.3", 25 | "eslint": "^5.16.0", 26 | "eslint-plugin-vue": "^5.2.3", 27 | "vue-cli-plugin-element": "^1.0.1", 28 | "vue-template-compiler": "^2.6.10" 29 | }, 30 | "eslintConfig": { 31 | "root": true, 32 | "env": { 33 | "node": true 34 | }, 35 | "extends": [ 36 | "plugin:vue/essential", 37 | "eslint:recommended" 38 | ], 39 | "parserOptions": { 40 | "parser": "babel-eslint" 41 | } 42 | }, 43 | "postcss": { 44 | "plugins": { 45 | "autoprefixer": {} 46 | } 47 | }, 48 | "browserslist": [ 49 | "> 1%", 50 | "last 2 versions", 51 | "not ie <= 8" 52 | ] 53 | } 54 | -------------------------------------------------------------------------------- /CTAI_web/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sharpiless/PaddleX-Flask-VUE-demo/16ef48cea78e7f2d5c95279787c5103568aae3cd/CTAI_web/public/favicon.ico -------------------------------------------------------------------------------- /CTAI_web/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | my-project 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /CTAI_web/src/App.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 26 | 27 | 29 | -------------------------------------------------------------------------------- /CTAI_web/src/assets/style.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: "iconfont"; /* project id 1171507 */ 3 | src: url("//at.alicdn.com/t/font_1171507_ki84ccb8hzk.eot"); 4 | src: url("//at.alicdn.com/t/font_1171507_ki84ccb8hzk.eot?#iefix") 5 | format("embedded-opentype"), 6 | url("//at.alicdn.com/t/font_1171507_ki84ccb8hzk.woff2") format("woff2"), 7 | url("//at.alicdn.com/t/font_1171507_ki84ccb8hzk.woff") format("woff"), 8 | url("//at.alicdn.com/t/font_1171507_ki84ccb8hzk.ttf") format("truetype"), 9 | url("//at.alicdn.com/t/font_1171507_ki84ccb8hzk.svg#iconfont") format("svg"); 10 | } 11 | @font-face { 12 | font-family: "iconfont"; /* project id 1171507 */ 13 | src: url("//at.alicdn.com/t/font_1171507_1e4m208yftt.eot"); 14 | src: url("//at.alicdn.com/t/font_1171507_1e4m208yftt.eot?#iefix") 15 | format("embedded-opentype"), 16 | url("//at.alicdn.com/t/font_1171507_1e4m208yftt.woff2") format("woff2"), 17 | url("//at.alicdn.com/t/font_1171507_1e4m208yftt.woff") format("woff"), 18 | url("//at.alicdn.com/t/font_1171507_1e4m208yftt.ttf") format("truetype"), 19 | url("//at.alicdn.com/t/font_1171507_1e4m208yftt.svg#iconfont") format("svg"); 20 | } 21 | @font-face { 22 | font-family: "iconfont"; /* project id 1171507 */ 23 | src: url("//at.alicdn.com/t/font_1171507_2skehui6htm.eot"); 24 | src: url("//at.alicdn.com/t/font_1171507_2skehui6htm.eot?#iefix") 25 | format("embedded-opentype"), 26 | url("//at.alicdn.com/t/font_1171507_2skehui6htm.woff2") format("woff2"), 27 | url("//at.alicdn.com/t/font_1171507_2skehui6htm.woff") format("woff"), 28 | url("//at.alicdn.com/t/font_1171507_2skehui6htm.ttf") format("truetype"), 29 | url("//at.alicdn.com/t/font_1171507_2skehui6htm.svg#iconfont") format("svg"); 30 | } 31 | @font-face { 32 | font-family: "iconfont"; /* project id 1171507 */ 33 | src: url("//at.alicdn.com/t/font_1171507_m12v7ykool.eot"); 34 | src: url("//at.alicdn.com/t/font_1171507_m12v7ykool.eot?#iefix") 35 | format("embedded-opentype"), 36 | url("//at.alicdn.com/t/font_1171507_m12v7ykool.woff2") format("woff2"), 37 | url("//at.alicdn.com/t/font_1171507_m12v7ykool.woff") format("woff"), 38 | url("//at.alicdn.com/t/font_1171507_m12v7ykool.ttf") format("truetype"), 39 | url("//at.alicdn.com/t/font_1171507_m12v7ykool.svg#iconfont") format("svg"); 40 | } 41 | @font-face { 42 | font-family: 'iconfont'; /* project id 1171507 */ 43 | src: url('//at.alicdn.com/t/font_1171507_ptv9kzz2fu.eot'); 44 | src: url('//at.alicdn.com/t/font_1171507_ptv9kzz2fu.eot?#iefix') format('embedded-opentype'), 45 | url('//at.alicdn.com/t/font_1171507_ptv9kzz2fu.woff2') format('woff2'), 46 | url('//at.alicdn.com/t/font_1171507_ptv9kzz2fu.woff') format('woff'), 47 | url('//at.alicdn.com/t/font_1171507_ptv9kzz2fu.ttf') format('truetype'), 48 | url('//at.alicdn.com/t/font_1171507_ptv9kzz2fu.svg#iconfont') format('svg'); 49 | } 50 | @font-face { 51 | font-family: 'iconfont'; /* project id 1171507 */ 52 | src: url('//at.alicdn.com/t/font_1171507_99rgegvq24p.eot'); 53 | src: url('//at.alicdn.com/t/font_1171507_99rgegvq24p.eot?#iefix') format('embedded-opentype'), 54 | url('//at.alicdn.com/t/font_1171507_99rgegvq24p.woff2') format('woff2'), 55 | url('//at.alicdn.com/t/font_1171507_99rgegvq24p.woff') format('woff'), 56 | url('//at.alicdn.com/t/font_1171507_99rgegvq24p.ttf') format('truetype'), 57 | url('//at.alicdn.com/t/font_1171507_99rgegvq24p.svg#iconfont') format('svg'); 58 | } 59 | .iconfont { 60 | font-family: "iconfont" !important; 61 | font-size: 18px; 62 | font-style: normal; 63 | -webkit-font-smoothing: antialiased; 64 | -webkit-text-stroke-width: 0.2px; 65 | -moz-osx-font-smoothing: grayscale; 66 | } 67 | .iconfont_tele{ 68 | font-family: "iconfont" !important; 69 | font-size: 40px; 70 | font-style: normal; 71 | -webkit-font-smoothing: antialiased; 72 | -webkit-text-stroke-width: 0.2px; 73 | -moz-osx-font-smoothing: grayscale; 74 | } 75 | .iconfont_image { 76 | font-family: "iconfont" !important; 77 | font-size: 60px; 78 | font-style: normal; 79 | -webkit-font-smoothing: antialiased; 80 | -webkit-text-stroke-width: 0.2px; 81 | -moz-osx-font-smoothing: grayscale; 82 | } 83 | .iconfont_phone { 84 | font-family: "iconfont" !important; 85 | font-size: 22px; 86 | font-style: normal; 87 | -webkit-font-smoothing: antialiased; 88 | -webkit-text-stroke-width: 0.2px; 89 | -moz-osx-font-smoothing: grayscale; 90 | } 91 | -------------------------------------------------------------------------------- /CTAI_web/src/components/Content.vue: -------------------------------------------------------------------------------- 1 | 338 | 339 | 846 | 847 | 864 | 865 | 1115 | 1116 | 1117 | -------------------------------------------------------------------------------- /CTAI_web/src/components/Footer.vue: -------------------------------------------------------------------------------- 1 | 6 | 16 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /CTAI_web/src/components/Header.vue: -------------------------------------------------------------------------------- 1 | 17 | 33 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /CTAI_web/src/main.js: -------------------------------------------------------------------------------- 1 | // The Vue build version to load with the `import` command 2 | // (runtime-only or standalone) has been set in webpack.base.conf with an alias. 3 | import Vue from 'vue' 4 | import App from './App' 5 | import VueRouter from 'vue-router' 6 | import axios from 'axios' 7 | import Element from 'element-ui' 8 | import echarts from "echarts"; 9 | 10 | Vue.prototype.$echarts = echarts; 11 | import '../node_modules/element-ui/lib/theme-chalk/index.css' 12 | import '../src/assets/style.css' 13 | import './theme/index.css' 14 | 15 | Vue.use(Element) 16 | Vue.config.productionTip = false 17 | Vue.use(VueRouter) 18 | Vue.prototype.$http = axios 19 | 20 | const router = new VueRouter({ 21 | routes: [ 22 | {path: "/App", component: App, meta: {title: "眼疾辅助诊断系统"},}, 23 | ], 24 | mode: "history" 25 | }) 26 | 27 | // // 全局注册组件 28 | Vue.component("App", App); 29 | 30 | /* eslint-disable no-new */ 31 | new Vue({ 32 | el: '#app', 33 | router, 34 | render: h => h(App) 35 | }) 36 | -------------------------------------------------------------------------------- /CTAI_web/src/theme/fonts/element-icons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sharpiless/PaddleX-Flask-VUE-demo/16ef48cea78e7f2d5c95279787c5103568aae3cd/CTAI_web/src/theme/fonts/element-icons.ttf -------------------------------------------------------------------------------- /CTAI_web/src/theme/fonts/element-icons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sharpiless/PaddleX-Flask-VUE-demo/16ef48cea78e7f2d5c95279787c5103568aae3cd/CTAI_web/src/theme/fonts/element-icons.woff -------------------------------------------------------------------------------- /CTAI_web/vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | lintOnSave: undefined, 3 | publicPath: './', 4 | outputDir: undefined, 5 | assetsDir: undefined, 6 | runtimeCompiler: undefined, 7 | productionSourceMap: undefined, 8 | parallel: false, 9 | css: undefined 10 | } 11 | -------------------------------------------------------------------------------- /CTAI_web/vue.md: -------------------------------------------------------------------------------- 1 | @[TOC](VUE开发入门——手把手教你做一个备忘录网站) 2 | 3 | # 本文禁止转载,违者必究! 4 | # 1. 前言: 5 | 最近上了移动互联应用开发的专选课,结果大作业要求做一个前后端分离的项目。这边我是个小白,所以要从头开始学,目前打算做一个后端用Flask、前端用VUE的智能医疗项目。 6 | 7 | 因为医疗图像分割要用深度学习,所以用Python做后端开发比较方便,因此选用了Flask框架。而VUE是一套用于构建用户界面的渐进式框架。与其它大型框架不同的是,Vue 被设计为可以自底向上逐层应用。Vue 的核心库只关注视图层,不仅易于上手,还便于与第三方库或既有项目整合。另一方面,当与现代化的工具链以及各种支持类库结合使用时,Vue 也完全能够为复杂的单页应用提供驱动。 8 | 9 | 本博客主要是按照 [这个网站](https://developer.mozilla.org/zh-CN/) 的教程来的,里面写了项目完成的过程和自己的理解。 10 | 11 | 后记:大作业项目视频在这里~ 12 | 13 | [https://www.bilibili.com/video/BV1k54y1s7j9](https://www.bilibili.com/video/BV1k54y1s7j9) 14 | 15 | 16 | [video(video-xqrrSDwe-1611656761668)(type-bilibili)(url-https://player.bilibili.com/player.html?aid=843751491)(image-https://ss.csdn.net/p?http://i1.hdslb.com/bfs/archive/4bd96281cb10ce1d6cf25c824711cbd4724c52f3.jpg)(title-基于Paddle+Flask的眼部医疗辅助系统(前后端分离))] 17 | 18 | 19 | # 2. VUE简介: 20 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210126184505626.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NDkzNjg4OQ==,size_16,color_FFFFFF,t_70) 21 | 22 | Vue是一个现代JavaScript框架提供了有用的设施渐进增强——不像许多其他框架,您可以使用Vue增强现有的HTML。这使我们可以使用Vue作为jQuery等库的临时替代品。 23 | 24 | 也就是说,我们还可以使用Vue编写整个单页应用程序(SPAs)。这允许我们创建标记完全由Vue管理,可以提高开发人员的经验和性能在处理复杂的应用程序。当我们需要的时候它还允许您利用其他库对客户端路由和状态进行管理。此外,Vue需要“中间地带”的方法工具客户端路由和状态管理。虽然Vue核心团队维护了建议的函数库,但他们并没有直接捆绑到 Vue 里。这样我们就可以选择一个其他路由/状态管理库,来更好地适应您的应用程序。 25 | 26 | 除了允许我们逐步将Vue集成到您的应用程序中,Vue还提供了一种渐进的方式编写标记。像大多数框架,Vue通过组件允许我们创建可重用块标记。大多数时候,Vue组件是使用一个特殊的HTML模板的语法写的。当我们需要比HTML语法允许的更多的控制时,我们可以编写JSX或纯JavaScript函数来定义组件。 27 | 28 | # 3. VUE安装和环境配置: 29 | 要在现有站点中使用Vue,可以通过"script"元素在页面中直接使用。当使用JQuery这样的库将现有项目迁移到Vue时,这是一个很好的选择。通过这种方法,我们可以使用Vue的许多核心功能,例如属性、自定义组件和数据管理。 30 | 31 | - 开发环境版本,包含了有帮助的命令行警告: 32 | 33 | ```html 34 | 35 | ``` 36 | 37 | - 生产环境版本,优化了尺寸和速度: 38 | 39 | ```html 40 | 41 | ``` 42 | 43 | 要构建更复杂的应用程序,我们需要使用 Vue NPM package。这将允许我们使用Vue的高级功能并利用WebPack等捆绑包。 44 | 45 | 为了使使用Vue构建应用程序更容易,有一个CLI来简化开发过程。要使用npm软件包和CLI: 46 | 47 | - 安装Node.js: 48 | 49 | > 在官网 [https://nodejs.org/en/](https://nodejs.org/en/) 下载即可安装 50 | 51 | - 安装npm: 52 | 53 | > npm已经在Node.js安装的时候顺带装好了。我们在命令提示符或者终端输入npm -v,应该看到类似的输出: 54 | 55 | ```bash 56 | C:\>npm -v 57 | 4.1.2 58 | ``` 59 | 60 | - 安装CLI: 61 | 62 | ```bash 63 | npm install --global @vue/cli 64 | ``` 65 | 66 | # 4. 创建VUE项目——Hello World: 67 | ## 4.1 创建基础项目: 68 | 使用如下命令创建一个项目: 69 | 70 | ```bash 71 | vue create moz-todo-vue 72 | ``` 73 | 选择Default配置: 74 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210126185516273.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NDkzNjg4OQ==,size_16,color_FFFFFF,t_70) 75 | 76 | 然后脚手架工具就开始构建项目,并且安装所需的依赖: 77 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210126185627530.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NDkzNjg4OQ==,size_16,color_FFFFFF,t_70) 78 | 创建完成后,可以看到出现了一些文件: 79 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210126190206798.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NDkzNjg4OQ==,size_16,color_FFFFFF,t_70) 80 | ## 4.2 文件类型解析: 81 | 我们接下来列举一些比较重要的: 82 | - **.eslintrc.js:** 这个是 eslint 的配置文件,可以通过它来管理你的校验规则。 83 | - **babel.config.js:** 这个是 Babel 的配置文件,可以在开发中使用 JavaScript 的新特性,并且将其转换为在生产环境中可以跨浏览器运行的旧语法代码。你也可以在这个里配置额外的 babel 插件。 84 | - **.browserslistrc:** 这个是 Browserslist 的配置文件,可以通过它来控制需要对哪些浏览器进行支持和优化。 85 | - **public:** 这个目录包含一些在 Webpack 编译过程中没有加工处理过的文件(有一个例外:index.html 会有一些处理)。 86 | - **favicon.ico:** 这个是项目的图标,当前就是一个 Vue 的 logo。 87 | - **index.html:** 这是应用的模板文件,Vue 应用会通过这个 HTML 页面来运行,也可以通过 lodash 这种模板语法在这个文件里插值。 88 | - **src:** 这个是 Vue 应用的核心代码目录 89 | - **main.js:** 这是应用的入口文件。目前它会初始化 Vue 应用并且制定将应用挂载到 index.html 文件中的哪个 HTML 元素上。通常还会做一些注册全局组件或者添额外的 Vue 库的操作。 90 | - **App.vue:** 这是 Vue 应用的根节点组件,往下看可以了解更多关注 Vue 组件的信息。 91 | - **component:** 这是用来存放自定义组件的目录,目前里面会有一个示例组件。 92 | - **assets:** 这个目录用来存放像 CSS 、图片这种静态资源,但是因为它们属于代码目录下,所以可以用 webpack 来操作和处理。意思就是你可以使用一些预处理比如 Sass/SCSS 或者 Stylus。 93 | 94 | ## 4.3 .vue 文件(单文件组件): 95 | 就像很多其他的前端框架一样,组件是构建 Vue 应用中非常重要的一部分。组件可以把一个很大的应用程序拆分为独立创建和管理的不相关区块,然后彼此按需传递数据,这些小的代码块可以方便更容易的理解和测试。 96 | 97 | 在其他框架都鼓励把模板、逻辑和样式的代码区分成不同文件的时候,Vue 却反其道行之。使用单文件组件,Vue 把模板、相关脚本和 CSS 一起整合放在 .vue 结尾的一个单文件中。这些文件最终会通过 JS 打包工具(例如 Webpack)处理,这意味着你可以使用构建时工具。我们可以使用比如 Babel、TypeScript、SCSS 等来创建更多复杂的组件。 98 | 99 | 另外,使用 Vue CLI 创建的项目被配置为在开箱即用的情况下借助 Webpack 使用 .vue 文件。实际上,如果我们查看我们使用 CLI 创建的项目中的 src 文件夹,我们会看到第一个.vue 文件:App.vue: 100 | 101 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210126191710147.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NDkzNjg4OQ==,size_16,color_FFFFFF,t_70) 102 | 103 | 104 | 打开 App.vue 文件,可以看到有三部分组成 "template","script" 和 "style",分别包含了组件的模板、脚本和样式相关的内容。所有的单文件组件都是这种类似的基本结构。 105 | 106 | - "template" 包含了所有的标记结构和组件的展示逻辑。template 可以包含任何合法的 HTML,以及一些我们接下来要讲的 Vue 特定的语法。 107 | - "script" 包含组件中所有的非显示逻辑,最重要的是, "script" 标签需要默认导出一个 JS 对象。该对象是您在本地注册组件、定义属性、处理本地状态、定义方法等的地方。在构建阶段这个对象会被处理和转换(包含 template 模板)成为一个有 render() 函数的 Vue 组件。 108 | - 组件的 CSS 应该写在 "style" 标签里,如果我们添加了 scoped 属性,形如 "style scoped" ,Vue 会把样式的范围限制到单文件组件的内容里。 109 | 110 | ## 4.4 运行项目: 111 | 在终端输入: 112 | 113 | ```bash 114 | npm run serve 115 | ``` 116 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210126191945936.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NDkzNjg4OQ==,size_16,color_FFFFFF,t_70) 117 | 然后打开Localhost,就可以看到VUE的Hello World界面: 118 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210126192024440.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NDkzNjg4OQ==,size_16,color_FFFFFF,t_70) 119 | 120 | ## 4.5 打包项目: 121 | 在终端运行: 122 | 123 | ```bash 124 | npm run build 125 | ``` 126 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210126192352340.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NDkzNjg4OQ==,size_16,color_FFFFFF,t_70) 127 | 即可打包到dist文件夹: 128 | 129 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210126192401350.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NDkzNjg4OQ==,size_16,color_FFFFFF,t_70) 130 | # 5. 在网页中显示标题: 131 | 在前面我们讲过, "template" 包含了所有的标记结构和组件的展示逻辑,可以包含任何合法的 HTML。因此我们可以用 HTML 修改 "template" 以显示标题。 132 | 133 | 因此我们将 "template" 修改为: 134 | 135 | ```html 136 | 141 | ``` 142 | 143 | 这时我们重新运行项目(或者项目在一个终端一直开着): 144 | 145 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210126192922755.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NDkzNjg4OQ==,size_16,color_FFFFFF,t_70) 146 | 147 | 发现运行出错! 148 | 149 | 这是因为 VUE 中如果某一个组件导入(HelloWorld.vue)而未使用时,VUE 编译不会通过,因此我们要删掉 "script" 中的 HelloWorld,将其修改为: 150 | 151 | ```html 152 | 157 | ``` 158 | 159 | 这样就可以看到终端中编译通过: 160 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210126193115590.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NDkzNjg4OQ==,size_16,color_FFFFFF,t_70) 161 | 同时 Localhost 的内容几乎也会同时更新: 162 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210126193229991.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NDkzNjg4OQ==,size_16,color_FFFFFF,t_70) 163 | # 6. 添加待做事项: 164 | 这部分我们将从创建一个组件来表示待办事项列表中的每个项目开始。在这一过程中,我们将学习一些重要的概念,例如在其他组件中调用组件,通过道具向它们传递数据,以及保存数据状态。 165 | 166 | ## 6.1 创建一个ToDoItem组件: 167 | 让我们创建第一个组件,它将显示一个单一的待办事项。我们将用它来建立我们的待办事项列表。 168 | 169 | 1. 在我们的的 src/components 目录下,创建一个ToDoItem.vue的新文件。 170 | 2. 通过在文件顶部添加 "template""/template"来创建组件的模板部分。 171 | 在我们的模板部分下面创建一个"script""/script"部分。 172 | 3. 在"script"标签内,添加一个默认导出对象export default {},这就是我们的组件对象。 173 | 174 | 现在 ToDoItem.vue 应该是这样: 175 | 176 | ```html 177 | 178 | 181 | ``` 182 | 183 | 现在我们可以开始为ToDoItem添加实际内容了。Vue模板目前只允许一个根元素——一个元素需要包裹模板内的所有内容(Vue 3 后允许多个)。 184 | 185 | 我们将为该根元素使用一个"div"。 186 | 187 | 1. 现在在我们的组件模板中添加一个空的"div"。 188 | 189 | 2. 在"div"里面,让我们添加一个checkbox和一个对应的label。给复选框添加一个id,并添加一个for属性,将复选框映射到标签上。 190 | 191 | 现在 ToDoItem.vue 应该是这样: 192 | 193 | ```html 194 | 200 | ``` 201 | ## 6.2 在应用程序中使用TodoItem组件: 202 | 我们现在把TodoItem组件添加到我们的主程序中: 203 | 1. 再次打开App.vue文件。 204 | 2. 在"script"标签的顶部,添加以下内容来引入ToDoItem组件: 205 | 206 | 207 | > import ToDoItem from './components/ToDoItem.vue'; 208 | 209 | 3. 在我们的组件对象里面,添加 components 属性,然后在它里面添加我们的ToDoItem组件进行注册: 210 | > components: {ToDoItem,} 211 | 212 | 现在 App.vue 应该是这样: 213 | 214 | ```html 215 | 220 | 221 | 230 | 231 | 241 | 242 | ``` 243 | 244 | 而要在应用程序中实际展示ToDoItem组件,我们需要在"template"模板内添加一个"to-do-item""/to-do-item"元素。请注意,组件文件名及其在JavaScript中的表示方式总是用驼峰大写(例如ToDoList),而等价的自定义元素总是用连字符小写(例如"to-do-list"): 245 | 1. 在"h1"下面,创建一个无序列表("ul"),其中包含一个列表项("li")。 246 | 2. 在列表项("li")里面添加"to-do-item""/to-do-item"。 247 | 248 | 现在 App.vue 应该是这样: 249 | 250 | ```html 251 | 261 | 262 | 271 | 272 | 282 | 283 | ``` 284 | 如果我们再次查看你的应用程序的渲染情况,现在应该看的到渲染的ToDoItem组件,由一个复选框和一个标签组成: 285 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210126194531820.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NDkzNjg4OQ==,size_16,color_FFFFFF,t_70) 286 | ## 6.3 使用props使组件动态化: 287 | 这时我们会发现一个问题,就是我们的ToDoItem组件只能在页面上真正包含一次(ID必须是唯一的),并且我们无法设置标签文本,没有什么是动态的。 288 | 289 | 因此我们需要一些组件状态。这可以通过在我们的组件中添加props来实现。我们可以认为props类似于函数中的输入,props的值赋予组件一个影响其显示的初始状态。 290 | 291 | 在Vue中,有两种注册props的方法: 292 | 1. 第一种方法是将props列为字符串数组。数组中的每个条目都对应于props的名称。 293 | 2. 第二种方法是将props定义为一个对象,每个键对应于prop名称。将props列为对象可让我们指定默认值,根据需要标记props,执行基本的对象键入(特别是围绕JavaScript基本类型)以及执行简单的prop验证。 294 | 295 | 对于此组件,我们将使用对象注册方法。 296 | 1. 返回ToDoItem.vue文件。 297 | 2. 在导出default {}对象内部添加一个props属性,该属性包含一个空对象。 298 | 3. 在此对象内,使用键label和添加两个属性done,label代表待做事项的名称,done代表事情是否完成。 299 | 4. label键的值应是具有2个属性的对象(或props,因为他们是所谓的在被提供给部件的上下文中)。 300 | 1. 第一个是required属性,其值为true。这将告诉Vue,我们希望该组件的每个实例都有一个label字段。如果ToDoItem组件没有标签字段,Vue会警告我们。 301 | 2. 第二个是type属性。将此属性的值设置为JavaScriptString类型(注意,大写字母“ S”)。这告诉Vue,我们希望此属性的值为字符串。 302 | 5. 现在到done。 303 | 1. 首先添加一个default值为的字段false。这意味着,当没有任何done prop传递到ToDoItem组件时,done prop的默认值将为false。 304 | 2. 接下来,添加一个type值为的字段Boolean。这告诉Vue,我们期望value prob是JavaScript布尔类型。 305 | 306 | 现在 ToDoItem.vue 应该是这样: 307 | 308 | ```html 309 | 315 | 316 | 324 | ``` 325 | 326 | 327 | 通过在组件对象中定义这些道具,我们现在可以在模板中使用这些变量值。让我们从将labelprop添加到组件模板开始。 328 | 329 | 在 ToDoItem.vue 中的"template",用{{label}}替换"label"元素的内容,即将原本的: 330 | ```html 331 | 332 | ``` 333 | 334 | 改为: 335 | 336 | ```html 337 | 338 | ``` 339 | 340 | 由于我们将标记label为必需的道具,但我们从未提供该prop的默认值,因此在调用它时,需要将其传递给组件。 341 | 342 | 在App.vue 文件内部,向"to-do-item""to-do-item"组件添加一个label prop ,就像常规的HTML属性一样: 343 | 344 | ```html 345 | 346 | ``` 347 | 此时我们可以看到,通过label属性我们将字符串传到了ToDoItem组件: 348 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210126201020316.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NDkzNjg4OQ==,size_16,color_FFFFFF,t_70) 349 | ## 6.4 Vue的数据对象: 350 | 现在我们有一个带有可更新标签的复选框。但是,我们目前对“ done” prop做不了任何事情——尽管我们可以在UI中选中复选框,但是在应用程序的任何地方都没有记录待办事项是否确实完成。 351 | 352 | 为此,我们希望将组件的done prop绑定到"input"元素上的checked属性,以便它可以用作是否选中该复选框的记录。但是,在VUE中,prop必须作为一种单向数据绑定,即组件绝不能改变其自身prop的值。 353 | 354 | 要解决此问题,我们可以使用Vue的data属性来管理done状态。该data属性是我们可以在组件中管理本地状态的地方,它与props属性一起位于组件对象内部,并具有以下结构: 355 | 356 | ```html 357 | data() { 358 | return { 359 | key: value 360 | } 361 | } 362 | ``` 363 | 我们可以注意到该data属性是一个函数。这是为了在运行时使每个组件实例的数据值保持唯一性,即为每个组件实例分别调用该函数。如果将数据声明为仅一个对象,则该组件的所有实例将会共享相同的值,而这往往是我们不想要的。 364 | 365 | 我们可以使用this从内部数据访问组件的prop和其他属性。 366 | 367 | 因此,让我们向ToDoItem组件添加一个data属性。这将返回一个包含单个属性的对象,我们将其称为isDone,其值为this.done。 368 | 369 | 现在 ToDoItem.vue 应该是这样: 370 | 371 | ```html 372 | 378 | 379 | 392 | ``` 393 | Vue在这里做了一点处理——它将我们所有的props直接绑定到该组件实例,因此我们不必调用this.props.done。这就是为什么我们将data属性称为isDone而不是的原因done(即如果都称之为done,在其他组件中将无法区分)。 394 | 395 | 因此,现在我们需要将该isDone属性附加到组件。与Vue使用{{}}表达式在模板内显示JavaScript表达式的方式类似,Vue具有特殊的语法将JavaScript表达式绑定到HTML元素和组件:v-bind。该v-bind表达式如下所示: 396 | 397 | ```html 398 | v-bind:attribute="expression" 399 | ``` 400 | 401 | 换句话说,我们要为要绑定的任何属性/属性加上前缀v-bind:。在大多数情况下,我们可以使用该v-bind属性的简写,即在属性/prop前面加上一个冒号。因此 :attribute="expression" 与 v-bind:attribute="expression" 相同。 402 | 403 | 因此,对于我们ToDoItem组件中的复选框,我们可以使用v-bind将isDone属性映射到元素上的checked属性。以下两项是等效的: 404 | 405 | ```html 406 | 407 | 408 | 409 | ``` 410 | 现在我们更新 APP.vue ,在"input"元素中替换 checked="false" 为 :checked="isDone": 411 | 412 | ```html 413 | 414 | ``` 415 | 416 | 现在 App.vue 应该是这样: 417 | 418 | ```html 419 | 429 | 430 | 431 | 440 | 441 | 451 | 452 | ``` 453 | 454 | 此时我们看到网页待做事项默认变为未完成: 455 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210126203038565.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NDkzNjg4OQ==,size_16,color_FFFFFF,t_70) 456 | ## 6.5 给Todos一个唯一的ID: 457 | 我们目前只能ToDoList在页面上添加一个组件,因为组件id是硬编码的,而这将导致辅助技术出现错误,因为id需要将标签正确映射到其复选框。 458 | 459 | 要解决此问题,我们可以通过编程方式在组件数据中进行设置id。 460 | 461 | 我们可以使用lodash包的uniqueid()方法来帮助保持索引唯一。该软件包导出一个函数,该函数接受一个字符串,并将一个唯一的整数附加到前缀的末尾,从而保持组件id的唯一性。 462 | 463 | 让我们使用npm将软件包添加到我们的项目中。 464 | 465 | 首先停止服务器,然后在终端中输入以下命令: 466 | 467 | ```bash 468 | npm install --save lodash.uniqueid 469 | ``` 470 | 471 | 注意该包是安装到项目目录,新项目需要重新安装。 472 | 473 | 现在,我们可以将该包导入到我们的ToDoItem组件中。在ToDoItem.vue的"script"元素顶部添加: 474 | 475 | ```html 476 | import uniqueId from 'lodash.uniqueid'; 477 | ``` 478 | 479 | 接下来,在我们的data属性中添加一个id字段: 480 | 481 | ```html 482 | data() { 483 | return { 484 | isDone: this.done, 485 | id: uniqueId('todo-') 486 | }; 487 | } 488 | ``` 489 | 其中 uniqueId() 返回指定的前缀— 'todo-' —并附加一个唯一的字符串。 490 | 491 | 接下来,将id绑定到我们复选框的id属性和标签的for属性,更新现有id属性和for属性: 492 | 493 | ```html 494 | 500 | ``` 501 | 502 | 现在 ToDoItem.vue 应该是这样: 503 | 504 | ```html 505 | 511 | 512 | 527 | ``` 528 | 529 | # 7. 添加待做事项列表: 530 | 我们接下来添加更多的 ToDoItem 组件到我们的App。该部分中我们会添加一系列待办事项到App.vue组件,并使用v-for指令遍历这些函数,将它们的每一项展示在ToDoItem组件中。 531 | 532 | ## 7.1 利用v-for指令渲染列表: 533 | 一个有效的待办事项列表需要有多个被渲染的待办事项,Vue中的v-for 可以实现这种效果。它是Vue自带的指令,用于在模板中实现循环,我们可以利用它我们将利用其迭代待办事项列表,将其中的每一项展示为单独的ToDoItem组件。 534 | 535 | 首先我们需要准备一个待办​​事项清单。添加 data 属性到 App.vue组件对象中,它包含一个 ToDoItems细分,其值是待办事项清单。在最终完成添加新的待办事项功能之前,我们可以先添加一些待办项目,每个待办项目可以用一个对象表示,这个对象包含 name 和 done 属性: 536 | 537 | ```html 538 | export default { 539 | name: 'app', 540 | components: { 541 | ToDoItem 542 | }, 543 | data() { 544 | return { 545 | ToDoItems: [ 546 | { label: '完成移动互联应用大作业', done: false }, 547 | { label: '复习软件工程第九章', done: false }, 548 | { label: '快乐五排上分', done: true }, 549 | { label: '早睡早起身体倍棒', done: false } 550 | ] 551 | }; 552 | } 553 | }; 554 | ``` 555 | 556 | 现在我们有了一个列表,用可以v-for去展示它们了指令的作用英文方式状语从句:元素的属性类似,就V-的而言,它类似JS中的。for...in循环,v-for="item in items" ——iterms是你要迭代的列表, item 是数组中当前元素的引用。 557 | 558 | 在进行数据传递之前,我们要了解下一个key属性,它和v-for使用,以帮助Vue标识列表中的元素,这样Vue不需要在列表变化时重新创建它们。 559 | 560 | 但是Vue需要一个唯一的标识,我们可以使用 lodash.uniqueid() 来实现: 561 | 1. 导入 lodash.uniqueid 到 App 组件: 562 | 563 | 564 | ```html 565 | import uniqueId from 'lodash.uniqueid'; 566 | ``` 567 | 568 | 2. 添加 id 细分到细分 ToDoItems的每一个元素中,并且将他们赋值为 uniqueId('todo-'),例如: 569 | 570 | 571 | ```html 572 | { id: uniqueId('todo-'), label: '早睡早起身体倍棒', done: false }, 573 | ``` 574 | 575 | 3. 添加 v-for指令和 key属性到 "li" 元素: 576 | 577 | ```html 578 | 583 | ``` 584 | 585 | 现在当我们去看运行着的app时,我们会发现待办事项显示了它们自己正确的名字和属性: 586 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210126204755982.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NDkzNjg4OQ==,size_16,color_FFFFFF,t_70) 587 | ## 7.2 重构id属性: 588 | 因为我们已经要为每一个待办事项创建一个唯一id,所以不妨碍把id作为ToDoItem的一个prop,而无需在每个checkbox里生成它: 589 | 1. 添加一个新的propid 到 ToDoItem组件。 590 | 2. 标记它为必需,类型是 String 。 591 | 3. 为防止命名冲突,删除掉data属性中的id分支。 592 | 4. 删除掉 import uniqueId from 'lodash.uniqueid'; 这行。 593 | 594 | 现在 ToDoItem.vue 应该是这样: 595 | 596 | ```html 597 | 603 | 604 | 618 | ``` 619 | 620 | 渲染后的站点看起来是没有变化的,但是这次重组尝试item.id像其他参数一样,作为prop从App.vue传递给ToDoItem,从而使得代码变得逻辑性一致。 621 | 622 | # 8. 添加输入框: 623 | ## 8.1 渲染输入框: 624 | 接下来我们真正需要的是允许我们的用户在应用程序中输入自己的待办事项的功能,为此,我们需要一个text "input",一个在提交数据时触发的事件,一个在提交时触发的方法以添加数据并重新呈现列表,以及控制数据的模型。 625 | 626 | 让我们新建一个组件来允许我们添加新的待办项。 627 | 628 | 在components目录下,新建文件 ToDoForm.vue: 629 | 630 | ```html 631 | 632 | 633 | 636 | ``` 637 | 638 | 然后新建一个HTML表单来允许我们输入新的待办项并把它提交到app。我们需要一个 "form" ,它里面包含一个 "label",一个 "input",一个 "button"。更新后的模版如下: 639 | 640 | ```html 641 | 658 | ``` 659 | 660 | 现在我们有一个可以form组件可以用来输入新的待办项的标题,它最终会渲染成ToDoItem的标签。 661 | 662 | 我们把这个组件添加到app中。 663 | 664 | 返回 App.vue 然后在 "script" 添加以下的语句: 665 | 666 | ```html 667 | import ToDoForm from './components/ToDoForm'; 668 | ``` 669 | 670 | 在App组件中注册它: 671 | 672 | ```html 673 | components: { 674 | ToDoItem, 675 | ToDoForm, 676 | }, 677 | ``` 678 | 最后将 ToDoForm组件添加到App中的"template" 中,像下面这样: 679 | 680 | ```html 681 | 692 | ``` 693 | 现在,当我们查看运行中的站点时,应该会看到显示的新表单: 694 | 695 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210126210022825.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NDkzNjg4OQ==,size_16,color_FFFFFF,t_70) 696 | 如果我们填写表格并单击“添加”按钮,页面将把表格发回到服务器上,但这并不是我们真正想要的。 697 | 698 | 我们实际上想要做的是在submit事件上运行一个方法,该方法会将新的待办事项添加到ToDoItem内部定义的数据列表中App。为此,我们需要向组件实例添加一个方法。 699 | 700 | ## 8.2 使用v-on创建方法并将其绑定到事件: 701 | 为了提供给ToDoForm组件一个方法,我们需要将它添加到组件对象,这是一个内部完成methods属性作为我们的组件。 702 | 703 | 在此组件中,我们需要在组件对象内的属性中添加一个onSubmit()方法。我们将使用它来处理Submit操作: 704 | 705 | ```html 706 | 715 | ``` 716 | 接下来,我们需要将方法绑定到"form"元素的submit事件处理程序上。 717 | 718 | 就像Vue如何使用v-bind语法来绑定属性一样,Vue有一个特殊的指令用于事件处理:v-on。该v-on指令通过v-on:event="method"语法起作用。就像v-bind,还有一种简写语法:@event="method"。 719 | 720 | 然后将submit处理程序添加到"form"元素中,如下所示: 721 | 722 | ```html 723 |
724 | ``` 725 | 726 | 为了防止浏览器发布到服务器,我们需要停止事件在页面中冒泡的默认操作。Vue具有一种特殊的语法,称为事件修饰符,可以在模板中为我们处理此事件。 727 | 728 | 修饰符会附加到事件的末尾,并带有一个点,如下所示:@event.modifier。以下事件修饰符的列表: 729 | 730 | - .stop:阻止事件传播。等效Event.stopPropagation()于常规JavaScript事件。 731 | - .prevent:防止事件的默认行为。等同于Event.preventDefault()。 732 | - .self:仅当事件是从此确切元素调度的时才触发处理程序。 733 | - {.key}:仅通过指定的键触发事件处理程序。MDN包含有效键值的列表;多字键只需要转换为kebab大小写即可(例如page-down)。 734 | - .native:在组件的根(最外面的包装)元素上侦听本地事件。 735 | - .once:监听事件,直到事件被触发一次,然后不再触发。 736 | - .left:仅通过鼠标左键事件触发处理程序。 737 | - .right:仅通过鼠标右键事件触发处理程序。 738 | - .middle:仅通过鼠标中键事件触发处理程序。 739 | - .passive:等效于在使用的{ passive: true }JavaScript中创建事件监听器时使用参数addEventListener()。 740 | 741 | 在这种情况下,我们需要使用.prevent处理程序来停止浏览器的默认提交动作: 742 | 743 | ```html 744 | 745 | ``` 746 | 747 | 表示再点击提交按钮时,阻止事件传播,并激活onSubmit函数。 748 | 749 | 此时点击【提交】可以看到控制台有相应的输出,并且网页不再刷新: 750 | 751 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/2021012621085331.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NDkzNjg4OQ==,size_16,color_FFFFFF,t_70) 752 | 接下来,我们需要一种从表单中获取值的方法,以便"input"可以将新的待办事项添加到ToDoItems数据列表中。 753 | 754 | ## 8.3 从表单中获取指定值: 755 | 我们需要的第一件事是通过表单中的data属性来跟踪待办事项的值。 756 | 757 | data()向我们的ToDoForm组件对象添加一个返回label字段的方法。我们可以将的初始值设置为label空字符串: 758 | 759 | ```html 760 | 774 | ``` 775 | 776 | 现在,我们需要某种方式将 "input" 字段的值附加到该label字段。Vue为此有一个特殊的指令:v-model。v-model绑定到您在其上设置的data属性,并使它与"input"保持同步。v-model适用于所有各种输入类型,包括复选框,单选按钮和选择输入。为了使用v-model,我们向"input"中添加v-model="variable"的结构属性: 777 | 778 | ```html 779 | 785 | ``` 786 | 787 | 让我们在onSubmit()方法中来测试一下提交的数据的值。在组件中,使用this关键字访问数据属性。因此,我们label使用来访问我们的字段this.label: 788 | 789 | ```html 790 | methods: { 791 | onSubmit() { 792 | console.log("Label value: ", this.label); 793 | }, 794 | }, 795 | ``` 796 | 797 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210126211909695.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NDkzNjg4OQ==,size_16,color_FFFFFF,t_70) 798 | 可以看到我们的 label 值已经和输入框的内容同步。 799 | 800 | ## 8.4 使用修饰符更改v-model的行为: 801 | 与事件修饰符类似,我们还可以添加修饰符来更改v-model的行为。就我们而言,有两个值得考虑的问题。第一个,.trim将从输入之前或之后删除空格。我们可以修改添加到我们的v-model语句,像这样: 802 | 803 | ```html 804 | v-model.trim="label" 805 | ``` 806 | 807 | 我们应该考虑的第二个修饰符称为.lazy。v-model同步文本输入的值时,此修饰符会更改。如前所述,v-model同步通过使用事件更新变量来进行。对于文本输入,此同步是使用inputevent发生的。通常,这意味着Vue在每次击键后都会同步数据。该.lazy修改将导致v-model使用该change事件来代替。这意味着Vue仅在输入失去焦点或提交表单时才同步数据。对我们而言这是更合理的,因为我们只需要最终数据。 808 | 809 | 要同时使用.lazy修饰符和.trim修饰符,可以将它们链接起来,例如: 810 | 811 | ```html 812 | v-model.lazy.trim="label" 813 | ``` 814 | 815 | 此时我们将 "input" 修改为: 816 | 817 | ```html 818 | 825 | ``` 826 | 827 | ## 8.5 通过自定义事件将数据传递给父组件: 828 | 现在我们需要做的下一件事是将新创建的待办事项传递给我们的App组件。为此,我们可以让ToDoForm发出一个自定义事件来传递数据,并使用App监听它。这与HTML元素上的事件非常相似:子组件可以发出可通过v-on监听的事件。 829 | 830 | 831 | 在ToDoFormon的Submit事件下,让我们添加一个todo-added事件。自定义事件的发出方式如下: 832 | 833 | ```html 834 | this.$emit("event-name") 835 | ``` 836 | 837 | 因此我们替换在onSubmit()里面的console.log()为: 838 | 839 | ```html 840 | this.$emit("todo-added"); 841 | ``` 842 | 然后向App.vue添加一个methods属性addToDo(),如下所示: 843 | 844 | ```html 845 | export default { 846 | name: "app", 847 | components: { 848 | ToDoItem, 849 | ToDoForm, 850 | }, 851 | data() { 852 | return { 853 | ToDoItems: [ 854 | { id: uniqueId("todo-"), label: "完成移动互联应用大作业", done: false }, 855 | { id: uniqueId("todo-"), label: "复习软件工程第九章", done: false }, 856 | { id: uniqueId("todo-"), label: "快乐五排上分", done: true }, 857 | { id: uniqueId("todo-"), label: "早睡早起身体倍棒", done: false }, 858 | ], 859 | }; 860 | }, 861 | methods: { 862 | addToDo() { 863 | console.log("添加成功"); 864 | }, 865 | }, 866 | }; 867 | ``` 868 | 869 | 接下来,将事件todo-added的监听器添加到"to-do-form""/to-do-form"中,事件触发时将调用addToDo()方法。使用@简写,监听器将如下所示: 870 | 871 | ```html 872 | @todo-added="addToDo" 873 | ``` 874 | 875 | 因此我们将其修改为: 876 | 877 | ```html 878 | 879 | ``` 880 | 表示在监听到 todo-added 事件时,调用addToDo()方法: 881 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/202101262143375.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NDkzNjg4OQ==,size_16,color_FFFFFF,t_70) 882 | 883 | 但是现在我们仍然没有将任何数据传递回App.vue组件。 884 | 885 | 我们可以通过this.$emit()向ToDoForm组件中的函数传递其他参数来实现。 886 | 887 | 在这种情况下,当我们触发事件时,我们希望将label数据与之一起传递。这是通过将要传递的数据作为另一个参数包含在$emit()方法中来完成的,可以通过如下方法实现: 888 | 889 | ```html 890 | this.$emit("todo-added", this.label) 891 | ``` 892 | 893 | 这类似于JavaScript事件包含数据的方式,除了自定义Vue事件默认情况下不包含任何事件对象。这意味着发出的事件将直接匹配我们提交的任何对象。在本例中,我们的事件对象将只是一个字符串。 894 | 895 | 因此我们修改onSubmit函数: 896 | 897 | ```html 898 | onSubmit() { 899 | this.$emit('todo-added', this.label) 900 | } 901 | ``` 902 | 903 | 然后向我们的addToDo()方法中添加一个包含label新待办事项的参数: 904 | 905 | ```html 906 | methods: { 907 | addToDo(toDoLabel) { 908 | console.log('To-do added:', toDoLabel); 909 | } 910 | } 911 | ``` 912 | 可以看到输入的字符串作为参数,传递到了addToDo中: 913 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210126215034272.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NDkzNjg4OQ==,size_16,color_FFFFFF,t_70) 914 | 915 | ## 8.6 将新的待办事项添加到我们的数据中: 916 | 现在我们有了App.vue中ToDoForm的可用数据,我们需要将一个代表它的项目添加到ToDoItems数组中。这可以通过将新的待办事项对象推入包含我们的新数据的数组来完成。 917 | 918 | 首先重写addToDo()方法: 919 | 920 | ```html 921 | addToDo(toDoLabel) { 922 | this.ToDoItems.push({id:uniqueId('todo-'), label: toDoLabel, done: false}); 923 | } 924 | ``` 925 | 926 | 这会将 {id:uniqueId('todo-'), label: toDoLabel, done: false} 推送到数据列表中: 927 | 928 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210126215618892.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NDkzNjg4OQ==,size_16,color_FFFFFF,t_70) 929 | 930 | 在继续之前,让我们做进一步的改进。如果在输入为空时提交表单,则没有文本的待办事项仍会添加到列表中。为了解决这个问题,我们可以防止在名称为空时触发添加todo的事件。由于名称已被.trim伪指令修剪,因此我们只需要测试空字符串即可: 931 | 932 | 回到ToDoForm组件,并更新onSubmit(): 933 | 934 | ```html 935 | onSubmit() { 936 | if (this.label === "") { 937 | return; 938 | } 939 | this.$emit("todo-added", this.label); 940 | }, 941 | ``` 942 | 943 | 现在我们将无法将空项目添加到待办事项列表。 944 | 945 | ## 8.7 使用v-model更新的输入值: 946 | 我们的ToDoForm组件中还有另一件事需要修复——提交后,"input"仍然包含旧值。但这很容易解决——因为我们v-model用来将数据绑定到"input"in ToDoForm,如果我们将name参数设置为等于空字符串,那么输入也会更新。 947 | 948 | ```html 949 | onSubmit() { 950 | if(this.label === "") { 951 | return; 952 | } 953 | this.$emit('todo-added', this.label); 954 | this.label = ""; 955 | } 956 | ``` 957 | # 9. 使用CSS样式化Vue组件: 958 | Vue具有三种样式化应用程序的方法: 959 | 960 | 1. 外部CSS文件。 961 | 2. 单个文件组件(.vue文件)中的全局样式。 962 | 3. 单个文件组件中组件范围的样式。 963 | 964 | ## 9.1 外部CSS文件的样式: 965 | 在src/assets目录中创建一个名为的reset.css文件: 966 | 967 | ```css 968 | /*reset.css*/ 969 | /* RESETS */ 970 | *, 971 | *::before, 972 | *::after { 973 | box-sizing: border-box; 974 | } 975 | *:focus { 976 | outline: 3px dashed #228bec; 977 | } 978 | html { 979 | font: 62.5% / 1.15 sans-serif; 980 | } 981 | h1, 982 | h2 { 983 | margin-bottom: 0; 984 | } 985 | ul { 986 | list-style: none; 987 | padding: 0; 988 | } 989 | button { 990 | border: none; 991 | margin: 0; 992 | padding: 0; 993 | width: auto; 994 | overflow: visible; 995 | background: transparent; 996 | color: inherit; 997 | font: inherit; 998 | line-height: normal; 999 | -webkit-font-smoothing: inherit; 1000 | -moz-osx-font-smoothing: inherit; 1001 | -webkit-appearance: none; 1002 | } 1003 | button::-moz-focus-inner { 1004 | border: 0; 1005 | } 1006 | button, 1007 | input, 1008 | optgroup, 1009 | select, 1010 | textarea { 1011 | font-family: inherit; 1012 | font-size: 100%; 1013 | line-height: 1.15; 1014 | margin: 0; 1015 | } 1016 | button, 1017 | input { 1018 | /* 1 */ 1019 | overflow: visible; 1020 | } 1021 | input[type="text"] { 1022 | border-radius: 0; 1023 | } 1024 | body { 1025 | width: 100%; 1026 | max-width: 68rem; 1027 | margin: 0 auto; 1028 | font: 1.6rem/1.25 "Helvetica Neue", Helvetica, Arial, sans-serif; 1029 | background-color: #f5f5f5; 1030 | color: #4d4d4d; 1031 | -moz-osx-font-smoothing: grayscale; 1032 | -webkit-font-smoothing: antialiased; 1033 | } 1034 | @media screen and (min-width: 620px) { 1035 | body { 1036 | font-size: 1.9rem; 1037 | line-height: 1.31579; 1038 | } 1039 | } 1040 | /*END RESETS*/ 1041 | ``` 1042 | 然后在src/main.js文件中,如下导入reset.css文件: 1043 | 1044 | ```javascript 1045 | import './assets/reset.css'; 1046 | ``` 1047 | 1048 | 重置后的样式: 1049 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210126220827130.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NDkzNjg4OQ==,size_16,color_FFFFFF,t_70) 1050 | ## 9.2 向单个文件组件添加全局样式: 1051 | 我们也可以直接将它们添加到的"style"标签中,例如修改App.vue: 1052 | 1053 | ```css 1054 | 1164 | ``` 1165 | 效果如图: 1166 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210126221035247.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NDkzNjg4OQ==,size_16,color_FFFFFF,t_70) 1167 | 1168 | ## 9.3 在Vue中添加CSS类: 1169 | 我们应该应用按钮CSS类到"button"我们的ToDoForm组件。由于Vue模板是有效的HTML,因此可以通过在class=""元素中添加属性,以与纯HTML相同的方式进行操作。 1170 | 1171 | 比如: 1172 | 1173 | ```html 1174 | 1177 | ``` 1178 | 和 1179 | 1180 | ```html 1181 |