Yii2 + elementui2 + uni app Usage Summary

Posted by GetPutDelete on Tue, 08 Feb 2022 06:58:27 +0100

yii2 application and git setting

We have added api to the advanced template

5 applications and directory structure

Five applications are as follows

  • common I actually use two directories: config and models
    • config
      • bootstrap.php must be loaded at startup. Note that we have added the alias @ API setting Yii:: setalias ('@ API', dirname (dirname (_dir_)). '/ api');
      • codeception-local.php,test.php,test-local.php these tests are used. I'm useless
      • params.php contains the icon framework and bootstrap version configuration for kartik components I use, 'icon framework' = > 'FA', 'bsversion' = > '4 x',,params-local.php (I'm empty)
      • main.php is a common configuration for each application (the so-called common configuration means that frontend and backend will load these configurations): language, time zone, component, module, etc; main-local.php is also common to all applications, with the meaning of - local. This configuration is related to the local environment, that is, the development environment is different from the deployment target environment, so it is usually a common database component configuration
    • models
      • The database table corresponds to the model, such as user php
        Because it is usually used in User model class applications, I am lazy to provide some public functions as its static methods
     * Generate the completion path for the file with the specified file name
     * @param string $fileName    The name of the file that will be generated (or used)
     * @param string $subPath     Relative to the subdirectory of the base directory, the default is upload / directory
     * @param bool $absolute      true = File system absolute path false = Network URL
     * @param string $alias       Yii2 Alias, used to generate the basic directory. The default is @ webroot application document root
     * @return string|string[]    Full path file name
    public static function getFilePath($fileName, $subPath = 'upload/', $absolute = false, $alias = '@webroot')
        $absolutePath = Yii::getAlias($alias).'/'.$subPath.$fileName;
        $_SERVER['DOCUMENT_ROOT'] = rtrim($_SERVER['DOCUMENT_ROOT'], '/\\');
        return $absolute ? $absolutePath : str_replace($_SERVER['DOCUMENT_ROOT'], '', $absolutePath);

     * Generates an img tag for the specified uploaded file
     * @param $imageUrls  string  List of image URL s separated by separator
     * @param string $delimiter  Separator
     * @return string   html Text containing several < img / > tags
    public static function htmlImages($imageUrls, $delimiter = '|')
        $html = '';
        foreach (explode('|', $imageUrls) as $filename) {
            if (!empty($filename)) {
                $html .= Html::img(User::getFilePath($filename, 'upload/', false, '@web'),
                    ['style' => 'width:auto;height:auto;max-width:100%;max-height:100%']);
        return $html ? $html : '((none)<br>';

     * Delete the dirty file with the specified file name from the upload directory
     * @param $fileNames array File name array
    public static function deleteDirtyImages($fileNames)
        foreach ($fileNames as $fileName) {
            $fullPath = User::getFilePath($fileName, 'upload/', true);

     * Find the user object according to the user name and password and return the object (if the object exists but the access_token is empty, the access token will be generated synchronously for the object)
     * @param $username string  user name
     * @param $password string  password
     * @return User|null    User object
    public static function findByUsernamePassword($username, $password)
        $model = User::findOne(['username' => $username,
            'password_hash' => User::hashPassword($password), 'status' => User::STATUS_ACTIVE]);
        if ($model !== null) {
            if (empty($model->access_token)) {
                $model->access_token = Yii::$app->security->generateRandomString();
                if ($model->update(false, ['access_token']) == false)
                    return null;  // save token failed, return null
        return $model;

     * Regenerate access token access_token
     * @return false|int
    public function refreshAccessToken()
        $this->access_token = Yii::$app->security->generateRandomString();
        return $this->update(false, ['access_token']);
      • A model that is not a database table, such as the form model loginform php
      • Non model class, I use it for Excel output XLSXWriter Class is put here (this class is just a file, which can be php5. This class uses ziprarchive class, so composer.json adds "ext zip": "*" and the beginning of the file uses ziprarchive;), I added a static method before the markMergedCell() method
    static public function parseMergeRange($strMergeRange)
       // Parse A1:B3 into ['startcellrow' = > 0, 'startcellcolumn' = > 0, 'endcellrow' = > 1, 'endcellcolumn' = > 2]
       $result = [];
       list($leftTop, $rightBottom) = explode(':', $strMergeRange);
       $result['startCellColumn'] = ord($leftTop[0]) - ord('A');  // Up to 26 columns are supported!
       $result['startCellRow'] = substr($leftTop, 1) - 1;
       $result['endCellColumn'] = ord($rightBottom[0]) - ord('A');  // Up to 26 columns are supported!
       $result['endCellRow'] = substr($rightBottom, 1) - 1;
       return $result;

` ``

  • backend
    My backend is mainly about background data management. At present, there is no complete separation of front and back ends. If it is completely separated, only non-public interfaces (limited to the requests of backend applications) should be processed here
    • Generate Excel files and download them (controller sample, XLSXWriter supports php5, can be generated natively, and supports a large amount of data, but the function is relatively simple. Phoffice / phpspreadsheet supports reading templates and then modifying them, but php7.2 + is required)
    public function actionExcel($ids)  // XLSXWriter supports php5
        $this->layout = false;
        $models = DisinfectRecord::find()->where(['id' => explode(',', $ids)])->all();  // data
        if (count($models) == 0) return;  // Data is empty and cannot be downloaded
        $date1 = $models[0]->date;  $date2 = $models[count($models)-1]->date;
        if (strcmp($date1, $date2) > 0) {
            $dateFrom = $date2; $dateTo = $date1;
        } else {
            $dateFrom = $date1; $dateTo = $date2;
        $downloadFileName = 'Disinfection record#'. $models[0]->address.'#'.$dateFrom.' To '$ dateTo.'.xlsx';  //  file name
        $writer = new XLSXWriter();         //writer class
        //Set header for browser download
        header('Content-disposition: attachment; filename="'.XLSXWriter::sanitize_filename($downloadFileName).'"');
        header("Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
        header('Content-Transfer-Encoding: binary');
        header('Cache-Control: must-revalidate');
        header('Pragma: public');
        // Header, title of each column, data type
        $caption = [$models[0]->address . 'Disinfection record (orange area)'];
        $thead = [
            ['A' => 'date', 'B' => 'Start and end time', 'C' => '', 'D' => 'Range', 'E' => 'Disinfectant', 'F' => '', 'G' => '',
                'H' => 'Disinfection dose', 'I' => 'water consumption', 'J' => 'proportion', 'K' => 'Executor', 'L' => 'Supervisor', 'M' => 'remarks'],
            ['A' => '', 'B' => 'rise', 'C' => 'stop', 'D' => '', 'E' => 'name', 'F' => 'manufactor', 'G' => 'Batch number',
                'H' => '', 'I' => '', 'J' => '', 'K' => '', 'L' => '', 'M' => '']
        $theadStyle = ['font' => 'Song style','font-size' => 12,'font-style' => 'bold', 'fill' => '#eee',
            'halign' => 'center', 'border' => 'left,right,top,bottom'];
        $colDataType = ['A' => 'string', 'B' => 'string', 'C' => 'string', 'D' => 'string', 'E' => 'string',
            'F' => 'string', 'G' => 'string', 'H' => 'string', 'I' => 'string', 'J' => 'string',
            'K' => 'string', 'L' => 'string', 'M' => 'string'];
        $colWidth = ['A' => '16', 'B' => '10', 'C' => '10', 'D' => '12', 'E' => '12',
            'F' => '16', 'G' => '16', 'H' => '10', 'I' => '10', 'J' => '10',
            'K' => '10', 'L' => '10', 'M' => '20'];
        $dataRowStyle = ['height' => 20,'font-size' => 12, 'border' => 'left,right,top,bottom'];
        // Workbook name
        $sheet1 = 'sheet1';
        $writer->writeSheetHeader($sheet1, array_values($colDataType), ['suppress_row'=>true,
            'widths' => array_values($colWidth)] );
        // Write header row
        $writer->writeSheetRow($sheet1, $caption,
        // Write header row
        $writer->writeSheetRow($sheet1, array_values($thead[0]), $theadStyle);
        $writer->writeSheetRow($sheet1, array_values($thead[1]), $theadStyle);
        // Write data
       foreach ($models as $model) {
           $tempRow = [$model->date, substr($model->time_begin, 0, 5), substr($model->time_end, 0, 5),
               $model->area, $model->drug_name, $model->drug_manufacturer, $model->drug_batch_number,
               $model->quantity, $model->ratio, $model->water_used, $model->implementer, $model->supervisor, $model->note];
           $writer->writeSheetRow($sheet1, $tempRow, $dataRowStyle);
        //Merge cells, the first row of headers and header rows
        $mergeSettings = ['A1:M1', 'A2:A3', 'B2:C2', 'D2:D3', 'E2:G2', 'H2:H3', 'I2:I3',
            'J2:J3', 'K2:K3', 'L2:L3', 'M2:M3'];
       foreach ($mergeSettings as $mergeSetting) {
           $merge = XLSXWriter::parseMergeRange($mergeSetting) ;
           $writer->markMergedCell($sheet1, $merge['startCellRow'], $merge['startCellColumn'], $merge['endCellRow'], $merge['endCellColumn']);
       //Output document

    public function actionExcel($ids)  // phpoffice/PhpSpreadsheet requires php7 2+!
        $this->layout = false;
        $models = DisinfectRecord::find()->where(['id' => explode(',', $ids)])->all();
        if (count($models) == 0) return;
        // Create workbook object from template file (template file must exist)
        $spreadsheet = IOFactory::load(User::getFilePath('tpl_disinfect_record.xlsx', '/', true));
        $worksheet = $spreadsheet->getActiveSheet();
        $rowNo = 4;
        foreach ($models as $model) {
            $worksheet->getCell('A' . $rowNo)->setValue($model->date);
            $worksheet->getCell('B' . $rowNo)->setValue($model->time_begin);
            $worksheet->getCell('C' . $rowNo)->setValue($model->time_end);
            $worksheet->getCell('D' . $rowNo)->setValue($model->area);
            $worksheet->getCell('E' . $rowNo)->setValue($model->drug_name);
            $worksheet->getCell('F' . $rowNo)->setValue($model->drug_manufacturer);
            $worksheet->getCell('G' . $rowNo)->setValue($model->drug_batch_number);
            $worksheet->getCell('H' . $rowNo)->setValue($model->quantity);
            $worksheet->getCell('I' . $rowNo)->setValue($model->ratio);
            $worksheet->getCell('J' . $rowNo)->setValue($model->water_used);
            $worksheet->getCell('K' . $rowNo)->setValue($model->implementer);
            $worksheet->getCell('L' . $rowNo)->setValue($model->supervisor);
            $worksheet->getCell('M' . $rowNo)->setValue($model->note);
        // write
        $writer = new Xls($spreadsheet);
        $writer->setPreCalculateFormulas(false);  // Do not calculate the formula
        $fileName = rand(1000, 9999).'.xlsx';
        $fullPath = User::getFilePath($fileName, '/', true, '@runtime');
        // You must manually dereference the loop and free up memory
        // send
        $size = filesize($fullPath);
        $date1 = $models[0]->date;  $date2 = $models[count($models)-1]->date;
        if (strcmp($date1, $date2) > 0) {
            $dateFrom = $date2; $dateTo = $date1;
        } else {
            $dateFrom = $date1; $dateTo = $date2;
        $downloadFileName = 'Disinfection record#'. $model->address.'#'.$dateFrom.' To '$ dateTo.'.xlsx';
        header("Accept-length:" . $size);
        echo file_get_contents($fullPath);
  • frontend
    My frontend is mainly the visual display of data on the front desk. At present, there is no complete separation of front and back ends. If it is completely separated, only non-public interfaces (limited to the requests of frontend applications) should be processed here
  • console I use two directories
    • migrations here is database migration, that is/ Migration file created by yii migrate/create command
    • controllers can implement console commands here. For example, I usually establish a DbController class to randomize the data table or temporarily modify the data table
  • I think this part of api needs to be referred to yii2 as a micro framework Implement more reasonable api app
    The verification part mainly implements a base class AuthActiveController with verification (derived from yii\rest\ActiveController)
 * Class AuthActiveController  Using query parameter access_token is used as the base class controller for authentication
 * @package api\controllers
class AuthActiveController extends ActiveController
    public function behaviors()
        $behaviors = parent::behaviors();
        $behaviors['corsFilter'] = [
            'class' => Cors::class,
            'cors' => [
                'Origin' => ['*'],
                'Access-Control-Request-Method' => ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'],
                'Access-Control-Request-Headers' => ['*'],
                'Access-Control-Allow-Origin' => ['*'],         // Allow cross domain
                //'access control allow credentials' = > true, / / allow cookie s to cross domains
                'Access-Control-Max-Age' => 86400,
                'Access-Control-Expose-Headers' => [],
        //*Verification can be cancelled for test convenience
        $behaviors['authenticator'] = [
            'class' => CompositeAuth::class,
            'authMethods' => [
//                [
//                    'class' => HttpBasicAuth::class,
//                    'auth' => function ($username, $password) {
//                        return User::findByUsernamePassword($username, $password);
//                    }
//                ],
        return $behaviors;

Then other controllers derive from it. In the behavior definition, you can cancel the verification of some action s, such as

class DisinfectRecordController extends AuthActiveController
    public $modelClass = 'common\models\DisinfectRecord';

    public function behaviors()
        $behaviors = parent::behaviors();
        $currentAction = Yii::$app->controller->action->id;
        if (in_array($currentAction, ['get-options', 'list', 'fetch'])) {  // These action s cancel validation
        return $behaviors;
    // ........................

In addition to the above five directories, other subdirectories and files are

  • h5 at present, only h5 is used in uni app. The generated release is here, and soft links can also be considered
  • log this directory is used to store nginx or apache logs, and personal write logs can also be placed here
    nginx website configuration sites available / my_ app:
server	{
	charset utf-8;
	client_max_body_size 128M;

	listen 80;
	listen [::]:80;

	server_name gxlq_pig;
	access_log /home/zime/PhpstormProjects/gxlq_pig/log/access.log;
	error_log /home/zime/PhpstormProjects/gxlq_pig/log/error.log info;
	#	rewrite_log on;  # use rewrite log

	set $front_root /home/zime/PhpstormProjects/gxlq_pig/frontend/web;
	set $back_root /home/zime/PhpstormProjects/gxlq_pig/backend/web;
	set $api_root /home/zime/PhpstormProjects/gxlq_pig/api/web;
	set $h5_root /home/zime/PhpstormProjects/gxlq_pig/h5;
	set $prj_root /home/zime/PhpstormProjects/gxlq_pig;
	root $prj_root;

	location / {
	    root $front_root;

	    try_files $uri index.php?$args  /frontend/web/index.php?$args;

	    location ~ ^\.(js|css|png|jpg|gif|swf|ico|pdf|mov|fla|zip|rar)$ {
	        access_log off;
		expires 360d;

		rewrite ^/(.+)$ /web/$1 break;
		rewrite ^/(.+)/(.+)$ /web/$1/$2 break;
		try_files $uri =404;

	location /api {
	   rewrite ^(/api)/$ $1 permanent;
	   try_files $uri  /api/web/index.php?$args;

	location ~ ^/api/(.+\.(js|css|png|jpg|gif|swf|ico|pdf|mov|fla|zip|rar))$ {
	    access_log off;
	    expires 360d;

	    rewrite ^/api/(.+)$  /api/web/$1 break;
	    rewrite ^/api/(.+)/(.+)$ /api/web/$1/$2 break;
	    try_files $uri =404;

	location /h5 {
	  rewrite ^(/h5)/$ $1 permanent;
	  try_files $uri /h5/index.html?$args;

	location ~ ^/h5/(.+\.(js|css|ttf|png|jpg|gif|swf|ico|pdf|mov|fla|zip|rar))$ {
	  access_log off;
	  expires 360d;

	location /admin {
	    rewrite ^(/admin)/$ $1 permanent;
	    try_files $uri /backend/web/index.php?$args;

	location ~ ^/admin/(.+\.(js|css|png|jpg|gif|swf|ico|pdf|mov|fla|zip|rar|woff2|svg|eot|woff|ttf))$ {
	    access_log off;
	    expires 360d;

	    rewrite ^/admin/(.+)$  /backend/web/$1 break;
	    rewrite ^/admin/(.+)/(.+)$ /backend/web/$1/$2 break;
	    try_files $uri =404;


	# deny accessing php files for the /assets directory
	location ~ ^/assets/.*\.php$ {
	    deny all;

	# pass PHP scripts to FastCGI server
	location ~ \.php$ {
		fastcgi_pass unix:/var/run/php/php7.2-fpm.sock;
	#	fastcgi_pass;
		fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
		include  fastcgi_params;
		try_files $uri =404;

	location = /requirements.php {
	    deny all;

	# deny access to .htaccess files, if Apache's document root
	location ~ /\.(ht|svn|git) {
		deny all;
  • The vendor directory has nothing to say
  • Root file
    • yii ,yii.bat this is the Yii command file
    • composer.json,composer.lock these two files are not explained. There is nothing to explain
    • clean.php is mainly used for cleaning. It is usually cleaned up before backup or packaging archiving
      It is mainly necessary to delete some files. The file code is similar to the following:
// Clean up files for backup
$isLinux = (PHP_OS == 'Linux');

function recursiveRemoveDir($dir) { // $dir itself not deleted
    $h = opendir($dir);
    while (false !== $filename = readdir($h)) {
        if ($filename == '.' || $filename == '..') continue;
        if (is_dir($dir.'/'.$filename)) {
        } else {

if (file_exists(__DIR__.'/h5.zip'))  unlink(__DIR__.'/h5.zip');


    • (readme.md)
    • See the following git settings for the contents of the. gitignore file
    • (. htaccess) nginx may also use this file during route rewriting, so you need to put the following into the cloud machine h5 directory htaccess file
\# use mod_rewrite for pretty URL support
RewriteEngine on
#if a directory or a file exists, use the request directly
RewriteCond %{REQUEST_FILENAME}  !-f
RewriteCond %{REQUEST_FILENAME}  !-d
\# otherwise forward the request to index.html
RewriteRule  .  index.html
    Each application web Directory root **.htaccess** The content of the document is
# use mod_rewrite for pretty URL support
RewriteEngine on
#if a directory or a file exists, use the request directly
RewriteCond %{REQUEST_FILENAME}  !-f
RewriteCond %{REQUEST_FILENAME}  !-d
# otherwise forward the request to index.php
RewriteRule  .  index.php

git settings

Root directory gitignore file information

# log file

# old or backup file

# Phpstorm

# test file

# directory runtime and web/assets


# vendor

# tests

# upload

# my template

Some key points of using uni app

Although some functions of HBuilderX are not particularly satisfactory, especially the automatic completion of middle quotation marks and the automatic insertion of quotation marks at the end of line feed are too pit, the automatic formatting is not ideal, git has not been integrated, and there is no linux version, but it is the official development environment of uni app, which provides support for uni app to the greatest extent, and it is convenient to install plug-ins and so on.

directory structure

  • package.json,package-lock.json my app is a description of a package. In particular, I use components for image conversion and compression image-conversion Therefore, there will be more dependencies. This component is installed with npm or yarn
npm i image-conversion --save
# or 
yarn add image-conversion

package.json,package-lock.json will be modified, node_ There will be an additional subdirectory image conversion under the modules directory

  • node_modules node component directory, such as image conversion above
  • Under the directory of components, there are uni app components
  • uni_modules is in accordance with easy_ Com standard uni app components (generally, using uni app components includes three steps, that is, installing them into the above components directory, import ing the beginning part of vue components externally, registering the components in vue components, while easy_com standard components can be used directly as long as they are installed, omitting the next two steps)
  • manifest.json this is the release configuration file of the application. We use the basic configuration (application name, etc.) and h5 configuration (use the basic path of operation, I am / h5 /. That is, the released things are placed under the URL basic directory of / h5 /
  • common
    • config.js this file contains the configuration information of the project (public configuration for the code)
export default {
	webUrl: '',  //   '/api',  //  
	imageBaseUrl: '',  // '/../admin/upload',  //  
	imageTypes: {
		'png': 'image/png', 
		'jpg': 'image/jpeg',
		'jpeg': 'image/jpeg',
		'gif': 'image/gif'
  • pages.json each page and the lower tabBar must be registered in this file, such as page path pages / disable / add
  • uni.scss is a common built-in style variable in uni app, mainly including color and basic size. We can use these variables without import ing this file, but the style needs to be in scss format
  • Put static resources such as pictures and fonts in the static directory
  • store
    • index.js we use vuex to implement the store. The store instance mainly includes state and changes to realize the unified management of state
  • utils can put some utility functions or general error handling functions here
    • Utils.js export default {some public functions}. For example, I put in the function of generating image Url
    makeImageUrl(src, imageBaseUrl) {
		if (Array.isArray(src))  src = src[0]   // for blob:  [path]
		return src.indexOf('blob:') > -1 ? src : imageBaseUrl + '/' + src
  • App.vue (the root component of the main entrance is generally not modified), main JS start the main entry file and import some things in the import mode, vue prototype.$ Something = something registers something as an attribute or method shared by all vue instances (so that all vue instances can be used in this.$something, which means that the attribute or method introduced in this way is "class static" rather than instance, which is global to vue, but not JavaScript global). Finally
const app = new Vue({
  • Pages is basically developed in this directory. Generally, a subdirectory corresponds to a function module, and each Vue file in the subdirectory corresponds to one or several related case s, such as my pages / interference / add Vue and pages / disinfect / list vue
  • The release files generated by unpackage compilation will be in this directory

state and related mutation in store

The state in the store is equivalent to some variables controlled by the center. To modify it, you must use the corresponding mutation instead of direct assignment. Its general structure is

import Vue from 'vue'
import Vuex from 'vuex'


const store = new Vuex.Store({
	state: {
		token: '',
		userInfo: {},
		// .........
	mutations: {
		UPDATE_TOKEN(state, token) {
			token ? (state.token = token) : (state.token = '')
			uni.setStorageSync('USER_TOKEN', token)
		UPDATE_USER_INFO(state, user) {
			user ? (state.userInfo = user) : (state.userInfo = {})
			uni.setStorageSync('USER_INFO', user)
		// .......
		GET_APP_STORAGE(state) {
			const token = uni.getStorageSync('USER_TOKEN')
			token ? state.token = token : ''
			const userInfo = uni.getStorageSync('USER_INFO')
			userInfo ? state.userInfo = userInfo : {}
			// ............
		USER_LOGOUT(state) {
			state.token = ''
			state.userInfo = {}
			// .............
			//  ............

export default store	

In the above code, UPDATE_XXX modifies the variable in state and writes the variable value to localstorage and GET_APP_STORAGE always reads the saved variable value from localstorage to restore the state variable value, user_ The initial state of the log out variable includes the log out information and the log out information

Using store state variables in components

Two mapping components need to be import ed at the beginning of the external (it will be very troublesome not to use this mapping method)

	import {
	} from 'vuex'

Map the required store state variable to the calculation attribute of our component, and map the required mutation method to the component method

	export default {
		computed: {
			...mapState(['token', 'userInfo', 'companyId'])
		data() {
			// .......
		// .......
		methods: {
			...mapMutations(['UPDATE_PROGRESS_ZC', 'GET_APP_STORAGE']),	
			// .........	

Then, you can use state variables and mutation in other parts of the component like ordinary attributes and methods. Before reading the attributes, you'd better always call this.GET_APP_STORAGE(), because the value recovered from localstorage is reliable

Photo selection, compression, deselection / deletion, preview

I used it image-conversion , so on the outside

	import {compress, compressAccurately} from 'image-conversion'

Photo processing mainly uses the pictures in the media processing of uni app, which can be referred to https://uniapp.dcloud.io/api/media/image.
Attributes related to photo upload (I allow users to choose the compressed size, so there are more selection lists and indexes)

		data() {
			return {
				// .............
				imageBaseUrl: '',    //  When onload, read this information from the configuration and write it
				images: [],          //  Actual storage of photo url on the server
				imagesTemp: [],      //  The current situation (temporary list) of the photo url. Object is used for onload Assign deep copy from images
				sourceTypeIndex: 2,
				sourceType: ['photograph', 'album', 'Photos or albums'],
				sizeTypeIndex: 2,
				sizeType: ['compress', 'Original drawing', 'Compressed or original'],
				countIndex: 3,
				count: [1, 2, 3, 4],
				imageSizeLimits: [50000, 4000, 2000, 1000, 800, 500, 200, 120, 80],
				islIndex: 3,       // Default item 4 1000KB
				extensionDict: {}

The template code related to the image is similar to the following

				<view class="proof">
					<view v-if="imagesTemp" class="images" style="display: flex;">
						<view v-for="(image, index) in imagesTemp" :key="index">
							<view style="position: relative;">
								<image class="image" :src="(images.indexOf(image) > -1 ? imageBaseUrl + '/' : '') + image"
								 :data-src="image" @tap="previewImage"></image>
								<image v-if="true" class="image-x" src="../../static/icon/remove.png" @click="remove(index)"></image>
						<image v-if="imagesTemp.length < 4" class="image" src="../../static/icon/save.png" @click="chooseImage"></image>

The code to delete / deselect an image remove(index) is relatively simple, that is, uni Showmodal() displays the modal dialog box, which is used when determining_ self.imagesTemp.splice(index, 1) can be deleted from the temporary list.
The code of preview picture is also relatively simple

			previewImage: function(e) {
				let src = e.target.dataset.src 
				var current =  _self.$util.makeImageUrl(src, _self.imageBaseUrl)
					current: current,
					urls: _self.imagesTemp.map(v => {
						return _self.$util.makeImageUrl(v, _self.imageBaseUrl)

The code for selecting pictures is troublesome, mainly due to compression (the compression component seems to have a small bug, and there is a problem with png format support, so jpeg is used here, regardless of the original photo format)

            chooseImage: function() {
					sourceType: sourceType[this.sourceTypeIndex],
					sizeType: sizeType[this.sizeTypeIndex],
					count: this.imagesTemp.length + this.count[this.countIndex] > 4 ? 4 - this.imagesTemp.length
						: this.count[this.countIndex],  // It seems that only 4 - this imagesTemp. Length indicates how many more can be selected
					success: (res) => {
						let file = res.tempFiles[0]
						if (file.size > _self.imageSizeLimits[_self.islIndex] * 1000) {
							let extension = file.name.substr(1+file.name.lastIndexOf('.')).toLowerCase()
							let compressConfig = {
								size: _self.imageSizeLimits[_self.islIndex],
								accuracy: 0.9,
								type: 'image/jpeg'
							if (file.size > 8*1024*1024) compressConfig['width'] = 1920
							compressAccurately(file, compressConfig).then(res => {
								let blobUrl = URL.createObjectURL(res)
								this.extensionDict[blobUrl] = 'jpg' // extension / / the suffix is lost when uploading after compression
						} else {  // For uncompressed original images, you can also use file Path = res.tempfilepaths [0] get blob URL
							this.imagesTemp = this.imagesTemp.concat([res.tempFilePaths]);
					fail: (err) => {}

Form submission and photo upload

The photos seen during preview may be the original url of the remote server, or they may be just selected and uploaded locally. Therefore, the file paths should be processed separately

                let filePaths = []
				for (let v of _self.imagesTemp) {
					if (Array.isArray(v)) filePaths.push(v[0])
					else if (v.indexOf('blob:') > -1) filePaths.push(v)

Also calculate the photos existing on the original remote server, and then prepare to delete them (equivalent to the difference set of images and imagetemp)

				let setTemp = new Set(_self.imagesTemp)
				let filesToDelete = _self.images.filter(v => !setTemp.has(v)) // Pictures that existed before and need to be deleted now

Photos are usually large, and the possibility of upload failure is much higher than that of form data submission, so I use Uni Showloading() shows that the upload is in progress, and then packages all upload tasks into Promise object array to ensure that the form is saved after uploading

					title: 'Uploading'
				let promises = []  // It is used to package all upload tasks to ensure that the upload task is completed before saving the form
				for (let filePath of filePaths) {
					promises.push(_self.$http.upload('biosafety-check-zc/upload-score', {
						params: {
							'access-token': _self.token  //, 'XDEBUG_SESSION_START': 'PHPSTORM'
						name: 'dirty_images_paper_zc', // This name is required in the background and corresponds to the background category
						filePath: filePath,
						formData: {   // Data submitted with upload POST
							key: _self.token,
							order: _self.id,
							extension: (filePath in _self.extensionDict) ? _self.extensionDict[filePath] : ''
					}).catch(err => {
				Promise.all(promises).then(() => {
					uni.hideLoading()    // Manually close the load mode dialog box
					_self.$http.post('biosafety-check-zc/save-score', {
						key: _self.token,
						category: 'dirty_images_paper_zc',
						order: _self.id,
						id: _self.id,
						filesToDelete: filesToDelete,
						cachedNum: filePaths.length,      // The number of files cached in the background should be equal to the number of files uploaded
						score: _self.score
					}, {
						params: {
							'access-token': _self.token  //, 'XDEBUG_SESSION_START': 'PHPSTORM'
					}).then(res => {
						if (res.data.saveOk) {  // Save successfully, retrieve the data and clear the progress
								title: res.data.cachedNum == filePaths.length ? 'Saved successfully' : 'Incomplete uploaded file',
								duration: 2000,
								success() {
									setTimeout(function() {}, 2000);
							// Retrieve data
							_self.$http.get('/biosafety-check-zc/view', {
								params: {
									'access-token': _self.token,
									'id': _self.id
							}).then(res => {
								_self.images = res.data.urls_paper_table ? res.data.urls_paper_table.split('|') : [],
								Object.assign(_self.imagesTemp, _self.images) // deep copy
							}).catch(err => {
							_self.UPDATE_PROGRESS_ZC(null)  // clear progress
						} else {
								title: 'Save failed',
								duration: 2000,
								success() {
									setTimeout(function() {}, 2000);
					}).catch(err => {
				}).catch(err => {
					uni.hideLoading()   // If there is an error in uploading, you also need to manually close the loading mode dialog box

A plug-in is used to upload files luch-request , there will be one more JS in the root directory after installation_ SDK / luch request, in the main entry main JS import request from '@ / JS_ SDK / luch request / luch request ',

const http = (baseUrl) => {
	return new Request({
		baseURL: baseUrl,
		header: {
			'Access-Control-Allow-Origin': '*'
Vue.prototype.$http = http(config.webUrl)

Wrap the above code so that all components can this$ http. Get() / post() / upload()

Background processing upload, temporary storage and form saving

The key to upload is the name. The file object is generated according to the name. You can POST certain information and submit it with the upload. Because multiple files are uploaded, we temporarily store the relevant information in the lookup table. When the form is saved, check whether all uploads are successful. If not all uploads are successful, maintain the original image information (delete the temporary information instead of deleting the original one), Similar codes are as follows

    public function actionUploadScore()
        $posted = Yii::$app->request->post();
        $file = UploadedFile::getInstanceByName('dirty_images_paper_zc');
        if (empty($file) || empty($posted)) return ['uploadOk' => false];  // Upload failed, failed to generate file object
        $fileName = 'bsc_zc_'.date('YmdHis'). '_' . rand(1000, 9999) . '.' .
            ($posted['extension'] ? $posted['extension'] : $file->extension);
        $fullPath = User::getFilePath($fileName, 'upload/', true, '@backend/web');
        $uploadOk = $file->saveAs($fullPath);  // Save file to disk
        if ($uploadOk) {
            (new Lookup([
                'key' => $posted['key'],  // token
                'category' => 'dirty_images_paper_zc',
                'item' => $fileName,
                'order' => $posted['order']  // record_id
            ]))->insert(false);    // Register to Lookup
        return ['uploadOk' => $uploadOk];

    public function actionSaveScore()
        $posted = Yii::$app->request->post();
        $filesToDelete = $posted['filesToDelete'];
        $model = BiosafetyCheckZc::findOne($posted['id']);
        $pics = $model->urls_paper_table ? explode('|', $model->urls_paper_table) : []; // Original picture
        if (!empty($filesToDelete)) {
            $pics = array_diff($pics, $filesToDelete);  // Remove the picture to be deleted
        $cachedItems = Lookup::findAll(['key' => $posted['key'], 'category' => $posted['category'],
            'order' => $posted['order']]);
        foreach ($cachedItems as $cachedItem) {
            $pics[] = $cachedItem->item;  // Add temporary uploaded pictures
        $model->urls_paper_table = implode('|', $pics);
        $model->score = $posted['score'];
        $saveOk = false;
        if ($posted['cachedNum'] == count($cachedItems)) {  // Save the form only after all pictures are uploaded and temporarily saved
            $saveOk = $model->save();
            if ($saveOk == false) {
                foreach ($cachedItems as $cachedItem)
                    @unlink(User::getFilePath($cachedItem->item, 'upload/', true, '@backend/web'));
            } else {
                foreach ($filesToDelete as $fileToDelete)
                    @unlink(User::getFilePath($fileToDelete, 'upload/', true, '@backend/web'));
        Lookup::deleteAll(['key' => $posted['key'], 'category' => $posted['category'],
            'order' => $posted['order']]); // Clean up temporary information
        return ['score' => $model->score, 'saveOk' => $saveOk, 'cachedNum' => count($cachedItems)];

elementUI2 CDN usage points

At present, I only use elementUI2 locally, so I use it CDN mode element , that is, the link label introduces the element style index.css , script tag introduction vue.js And element index.js , if you use axios, you can use the script tag to import axios in CDN mode

I have just read a little bit about element, and this part needs to be supplemented