linyaostalker
5 years ago
105 changed files with 5536 additions and 669 deletions
-
76backend/controllers/ConfigController.php
-
61backend/controllers/SiteController.php
-
166backend/logic/PermissionManager.php
-
106backend/modules/file/logic/file/FileManager.php
-
6backend/modules/file/models/ars/File.php
-
6backend/modules/file/models/ars/TemFile.php
-
21backend/modules/goods/controllers/AttributeController.php
-
3backend/modules/goods/controllers/BrandController.php
-
90backend/modules/goods/controllers/CategoryController.php
-
97backend/modules/goods/controllers/GoodsController.php
-
95backend/modules/goods/controllers/ShopCategoryController.php
-
3backend/modules/goods/controllers/SupplierController.php
-
244backend/modules/goods/logic/goods/GoodsManager.php
-
22backend/modules/goods/migrations/m191211_060934_update_column_limit_count_in_table_atg_goods.php
-
22backend/modules/goods/migrations/m191211_092335_update_column_sort_order_in_table_atg_goods.php
-
41backend/modules/goods/migrations/m191217_091658_update_column_is_manual_in_table_atg_goods_sku.php
-
26backend/modules/goods/migrations/m191217_092101_add_column_sku_image_in_table_atg_goods_sku.php
-
5backend/modules/goods/models/ars/Attribute.php
-
24backend/modules/goods/models/ars/Category.php
-
10backend/modules/goods/models/ars/Goods.php
-
8backend/modules/goods/models/ars/GoodsSku.php
-
28backend/modules/goods/models/ars/ShopCategory.php
-
12backend/modules/goods/models/searchs/CategorySearch.php
-
27backend/modules/goods/models/searchs/GoodsSearch.php
-
13backend/modules/goods/views/attribute/_search.php
-
13backend/modules/goods/views/brand/_search.php
-
4backend/modules/goods/views/category/_form.php
-
13backend/modules/goods/views/category/_search.php
-
4backend/modules/goods/views/goods/_form.php
-
4backend/modules/goods/views/goods/_search.php
-
44backend/modules/goods/views/goods/create.php
-
71backend/modules/goods/views/goods/goods.php
-
6backend/modules/goods/views/goods/new_editor.php
-
4backend/modules/goods/views/goods/picture.php
-
41backend/modules/goods/views/goods/update.php
-
4backend/modules/goods/views/shop-category/_form.php
-
13backend/modules/goods/views/shop-category/_search.php
-
26backend/modules/goods/views/supplier/_search.php
-
175backend/modules/shop/controllers/AfterSaleController.php
-
179backend/modules/shop/controllers/CommentController.php
-
122backend/modules/shop/controllers/ExpressTemplateController.php
-
21backend/modules/shop/controllers/OrderController.php
-
7backend/modules/shop/controllers/TakingSiteController.php
-
139backend/modules/shop/logic/ShopManager.php
-
68backend/modules/shop/migrations/m191209_080245_update_table_after_sale.php
-
20backend/modules/shop/migrations/m191217_013625_add_column_sku_value_in_table_ats_cart.php
-
20backend/modules/shop/migrations/m191217_021455_update_column_star_in_table_ats_comment.php
-
100backend/modules/shop/models/ars/AfterSale.php
-
4backend/modules/shop/models/ars/Cart.php
-
13backend/modules/shop/models/ars/Comment.php
-
6backend/modules/shop/models/ars/OrderGoods.php
-
188backend/modules/shop/models/searchs/AfterSaleSearch.php
-
186backend/modules/shop/models/searchs/CommentSearch.php
-
58backend/modules/shop/views/after-sale/_form.php
-
62backend/modules/shop/views/after-sale/_search.php
-
18backend/modules/shop/views/after-sale/create.php
-
31backend/modules/shop/views/after-sale/index.php
-
19backend/modules/shop/views/after-sale/update.php
-
108backend/modules/shop/views/after-sale/view.php
-
32backend/modules/shop/views/comment/_form.php
-
49backend/modules/shop/views/comment/_search.php
-
18backend/modules/shop/views/comment/create.php
-
31backend/modules/shop/views/comment/index.php
-
19backend/modules/shop/views/comment/update.php
-
47backend/modules/shop/views/comment/view.php
-
9backend/modules/shop/views/express-template/express_area_view.php
-
13backend/modules/shop/views/taking-site/_search.php
-
4backend/modules/shop/views/taking-site/create.php
-
27backend/views/config/index.php
-
52backend/views/layouts/sidebar.php
-
17backend/web/.babelrc
-
26backend/web/.eslintrc.json
-
11backend/web/.gitignore
-
89backend/web/package.json
-
1backend/web/src/iconfont.html
-
4backend/web/src/import.html
-
36backend/web/src/js/amazing-creator/GridEditor.js
-
101backend/web/src/js/amazing-creator/Module.js
-
106backend/web/src/js/amazing-creator/Previewer.js
-
16backend/web/src/js/amazing-creator/PropsEditor.js
-
29backend/web/src/js/amazing-creator/index.js
-
67backend/web/src/js/amazing-creator/modules/GoodsList.js
-
14backend/web/src/js/amazing-creator/modules/Swiper.js
-
150backend/web/src/js/amazing-creator/old.js
-
68backend/web/src/js/amazing-creator/store.js
-
313backend/web/src/sku-item/index.js
-
74backend/web/src/sku/SelectCell.js
-
157backend/web/src/sku/index.js
-
83backend/web/src/styles/amazing-creator.scss
-
90backend/web/src/styles/content-header.scss
-
180backend/web/src/styles/dashboard.scss
-
183backend/web/src/styles/global.scss
-
128backend/web/src/styles/header.scss
-
7backend/web/src/styles/index.scss
-
193backend/web/src/styles/login.scss
-
201backend/web/src/styles/sidebar.scss
-
30backend/web/src/styles/variables.scss
-
301backend/web/src/styles/widget.scss
-
76backend/web/src/utils/ajax.js
-
84backend/web/webpack.config.js
@ -1,76 +0,0 @@ |
|||
<?php |
|||
|
|||
namespace backend\controllers; |
|||
|
|||
use Yii; |
|||
use backend\models\ars\Category; |
|||
use yii\web\Controller; |
|||
use yii\web\NotFoundHttpException; |
|||
use yii\filters\VerbFilter; |
|||
|
|||
/** |
|||
* CategoryController implements the CRUD actions for Category model. |
|||
*/ |
|||
class ConfigController extends Controller |
|||
{ |
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function behaviors() |
|||
{ |
|||
return [ |
|||
'verbs' => [ |
|||
'class' => VerbFilter::className(), |
|||
'actions' => [ |
|||
'delete' => ['POST'], |
|||
], |
|||
], |
|||
]; |
|||
} |
|||
|
|||
/** |
|||
* Lists all Category models. |
|||
* @return mixed |
|||
*/ |
|||
public function actionIndex() |
|||
{ |
|||
return $this->render('index'); |
|||
} |
|||
|
|||
|
|||
/** |
|||
* Updates an existing Category model. |
|||
* If update is successful, the browser will be redirected to the 'view' page. |
|||
* @param integer $id |
|||
* @return mixed |
|||
* @throws NotFoundHttpException if the model cannot be found |
|||
*/ |
|||
public function actionUpdate($id) |
|||
{ |
|||
$model = $this->findModel($id); |
|||
|
|||
if ($model->load(Yii::$app->request->post()) && $model->save()) { |
|||
return $this->redirect('index'); |
|||
} |
|||
|
|||
return $this->render('update', [ |
|||
'model' => $model, |
|||
]); |
|||
} |
|||
|
|||
/** |
|||
* Finds the Category model based on its primary key value. |
|||
* If the model is not found, a 404 HTTP exception will be thrown. |
|||
* @param integer $id |
|||
* @return Category the loaded model |
|||
* @throws NotFoundHttpException if the model cannot be found |
|||
*/ |
|||
protected function findModel($id) |
|||
{ |
|||
if (($model = Category::findOne($id)) !== null) { |
|||
return $model; |
|||
} |
|||
|
|||
throw new NotFoundHttpException('The requested page does not exist.'); |
|||
} |
|||
} |
@ -0,0 +1,166 @@ |
|||
<?php |
|||
namespace backend\logic; |
|||
|
|||
use ReflectionClass; |
|||
use ReflectionException; |
|||
use ReflectionMethod; |
|||
use yii; |
|||
|
|||
class PermissionManager |
|||
{ |
|||
/** |
|||
* 获取app的已做标记控制器的权限 |
|||
* DESCRIBE 控制器描述,只有控制器的注释添加了该描述,才会自动匹配该控制器;使用例子:@DESCRIBE {controllerDescribe} DESCRIBE |
|||
* ACTION 方法描述,只有控制器下action方法的注释添加了该描述,才会自动匹配该action方法;使用例子:@ACTION {actionDescribe} ACTION |
|||
* 匹配控制器中actions的方法,需要添加id参数并且在标明含义,例子: |
|||
* '{actionName}' => [ |
|||
* 'class' => 'xxx\xxx\xxx', |
|||
* 'id' => '{actionDescribe}' |
|||
* ] |
|||
* @return array 返回已匹配到的权限。返回例子:[["{controllerDescribe}"=>["{actionDescribe}"=>"action route"]]] |
|||
* @throws yii\base\InvalidConfigException |
|||
* @throws ReflectionException |
|||
*/ |
|||
public static function getAppPermission() |
|||
{ |
|||
$permission = []; |
|||
$permission = self::getControllersAndActions($permission); //获取该app的所有controller权限
|
|||
$permission = self::getModuleControllerAndAction($permission); //获取该app引用module的所有controller权限
|
|||
return $permission; |
|||
} |
|||
|
|||
/** |
|||
* 获取app下controller已标记的action方法权限 |
|||
* 这个方法先是获取该app下controller的路径,控制器的命名空间,然后匹配该路径下面的所有控制器文件 |
|||
* 截取控制器文件的控制器基础名称,转换为相应url规则名称,拼接为相应类名 |
|||
* 根据控制器id,通过[[Module::createControllerByID]]方法实例化该控制器,最后通过[[PermissionManager::constructingPermissionArray]]获取所有已标记的权限 |
|||
* @param array $permission 权限数组 |
|||
* @return array 权限数组 |
|||
* @throws yii\base\InvalidConfigException |
|||
* @throws ReflectionException |
|||
*/ |
|||
private static function getControllersAndActions($permission = []) |
|||
{ |
|||
$dir = Yii::$app->getControllerPath(); |
|||
$nameSpace = Yii::$app->controllerNamespace . "\\"; |
|||
$fileList = glob($dir."/*Controller.php"); |
|||
foreach ($fileList as $file) { |
|||
$baseName = substr(basename($file), 0, -14); |
|||
|
|||
//根据路由规则转换控制器名称
|
|||
$name = strtolower(preg_replace('/(?<![A-Z])[A-Z]/', ' \0', $baseName)); |
|||
$id = ltrim(str_replace(' ', '-', $name), '-'); |
|||
|
|||
$className = $nameSpace . $baseName . 'Controller'; |
|||
$controller = Yii::$app->createControllerByID($id); |
|||
$permission = self::constructingPermissionArray($controller, $className, $permission); |
|||
} |
|||
return $permission; |
|||
} |
|||
|
|||
/** |
|||
* 获取该app下关联module的已标记controller的权限 |
|||
* 这个方法通过[[Module::getModules]]获取该app下关联的module并循环 |
|||
* 去除gii和debug,通过[[Module::getModule]]获取子类module,获取命名空间,文件位置,然后匹配该路径下面的所有控制器文件 |
|||
* 截取控制器文件的控制器基础名称,转换为相应url规则名称,拼接为相应类名 |
|||
* 通过[[BaseYii::createObject]]实例化,最后通过[[PermissionManager::constructingPermissionArray]]获取所有已标记的权限 |
|||
* @param array $permission 权限数组 |
|||
* @return array 权限数组 |
|||
* @throws yii\base\InvalidConfigException |
|||
* @throws ReflectionException |
|||
*/ |
|||
private static function getModuleControllerAndAction($permission = []) |
|||
{ |
|||
foreach (Yii::$app->getModules() as $id => $child) { |
|||
if(in_array($id, ['gii', 'debug'])) { |
|||
continue; |
|||
} |
|||
$module = Yii::$app->getModule($id); |
|||
$nameSpace = $module->controllerNamespace."\\"; |
|||
$dir = $module->controllerPath; |
|||
$fileList = glob($dir."/*Controller.php"); |
|||
foreach ($fileList as $file) { |
|||
$baseName = substr(basename($file), 0, -14); |
|||
$name = strtolower(preg_replace('/(?<![A-Z])[A-Z]/', ' \0', $baseName)); |
|||
$id = $module->id.'/'.ltrim(str_replace(' ', '-', $name), '-'); |
|||
$className = $nameSpace . $baseName . 'Controller'; |
|||
$controller = Yii::createObject($className, [$id, $module]); |
|||
$permission = self::constructingPermissionArray($controller, $className, $permission); |
|||
} |
|||
} |
|||
return $permission; |
|||
} |
|||
|
|||
/** |
|||
* 构建权限数组 |
|||
* 根据类名,使用ReflectionClass方法获取该类的信息 |
|||
* 通过该类的注释,判断是否存在DESCRIBE标记,以此判断是否要记录该类的权限 |
|||
* 若存在标记,则通过[[PermissionManager::getActionsInController]]获取actions里面的方法权限数组,通过[[PermissionManager::getActionInController]]获取该类下面的action方法权限数组 |
|||
* @param $controllerObject |
|||
* @param $className |
|||
* @param array $permission 权限数组 |
|||
* @return array 权限数组 |
|||
* @throws ReflectionException |
|||
*/ |
|||
private static function constructingPermissionArray($controllerObject, $className, $permission = []) |
|||
{ |
|||
$prefix = '/'.$controllerObject->id.'/'; |
|||
$reflection = new ReflectionClass($className); //通过ReflectionClass方法获取该类的所有信息,包括参数方法等
|
|||
$controllerComment = $reflection->getDocComment(); |
|||
$controllerPregRes = preg_match("/(?<=@DESCRIBE ).*?(?= DESCRIBE)/", $controllerComment, $controllerDescribe); |
|||
if ($controllerPregRes) { |
|||
$permission = self::getActionsInController($controllerObject, $controllerDescribe, $prefix, $permission); |
|||
$permission = self::getActionInController($className, $prefix, $controllerDescribe, $permission); |
|||
} |
|||
return $permission; |
|||
} |
|||
|
|||
/** |
|||
* 获取控制器类中actions的方法权限 |
|||
* 根据实例化的控制器类,获取actions的方法并且循环,方法存在id这个参数,则记录 |
|||
* 最后放回权限数组 |
|||
* @param $controllerObject |
|||
* @param $controllerDescribe |
|||
* @param $prefix |
|||
* @param array $permission 权限数组 |
|||
* @return array 权限数组 |
|||
*/ |
|||
private static function getActionsInController($controllerObject, $controllerDescribe, $prefix, $permission = []) |
|||
{ |
|||
foreach ($controllerObject->actions() as $id => $item) { |
|||
if (isset($item['id'])) { |
|||
$permission[$controllerDescribe[0]][$item['id']] = $prefix . $id; |
|||
} |
|||
} |
|||
return $permission; |
|||
} |
|||
|
|||
/** |
|||
* 获取控制器类中的action方法权限 |
|||
* 通过ReflectionClass方法获取所有的action方法,若注释中存在ACTION标记,则记录 |
|||
* @param $className |
|||
* @param $prefix |
|||
* @param $controllerDescribe |
|||
* @param array $permission 权限数组 |
|||
* @return array 权限数组 |
|||
* @throws ReflectionException |
|||
*/ |
|||
private static function getActionInController($className, $prefix, $controllerDescribe, $permission = []) |
|||
{ |
|||
$reflection = new ReflectionClass($className); //通过ReflectionClass方法获取该类的所有信息,包括参数方法等
|
|||
foreach ($reflection->getMethods(ReflectionMethod::IS_PUBLIC) as $method) { |
|||
//action的注释
|
|||
$actionComment = $method->getDocComment(); |
|||
$actionPregRes = preg_match("/(?<=@ACTION ).*?(?= ACTION)/", $actionComment, $actionDescribe); |
|||
if ($actionPregRes) { |
|||
$actionName = $method->getName(); |
|||
if ($actionName != 'actions' && strpos($actionName, 'action') === 0) { |
|||
$name = strtolower(preg_replace('/(?<![A-Z])[A-Z]/', ' \0', substr($actionName, 6))); |
|||
$id = $prefix . ltrim(str_replace(' ', '-', $name), '-'); |
|||
$permission[$controllerDescribe[0]][$actionDescribe[0]] = $id; |
|||
} |
|||
} |
|||
} |
|||
return $permission; |
|||
} |
|||
} |
@ -0,0 +1,22 @@ |
|||
<?php |
|||
|
|||
use yii\db\Migration; |
|||
|
|||
/** |
|||
* Class m191211_060934_update_column_limit_count_in_table_atg_goods |
|||
*/ |
|||
class m191211_060934_update_column_limit_count_in_table_atg_goods extends Migration |
|||
{ |
|||
public function up() |
|||
{ |
|||
$this->dropColumn('atg_goods', 'limit_count'); |
|||
$this->addColumn('atg_goods', 'limit_count', $this->integer(11)->defaultValue("0")->comment("限购数量")); |
|||
} |
|||
|
|||
public function down() |
|||
{ |
|||
$this->dropColumn('atg_goods', 'limit_count'); |
|||
$this->addColumn('atg_goods', 'limit_count', $this->integer(11)->defaultValue(null)->comment("限购数量")); |
|||
return true; |
|||
} |
|||
} |
@ -0,0 +1,22 @@ |
|||
<?php |
|||
|
|||
use yii\db\Migration; |
|||
|
|||
/** |
|||
* Class m191211_092335_update_column_sort_order_in_table_atg_goods |
|||
*/ |
|||
class m191211_092335_update_column_sort_order_in_table_atg_goods extends Migration |
|||
{ |
|||
public function up() |
|||
{ |
|||
$this->dropColumn('atg_goods', 'sort_order'); |
|||
$this->addColumn('atg_goods', 'sort_order', $this->smallInteger(3)->defaultValue("999")->comment('排序')); |
|||
} |
|||
|
|||
public function down() |
|||
{ |
|||
$this->dropColumn('atg_goods', 'sort_order'); |
|||
$this->addColumn('atg_goods', 'sort_order', $this->smallInteger(3)->defaultValue(null)->comment('排序')); |
|||
return true; |
|||
} |
|||
} |
@ -0,0 +1,41 @@ |
|||
<?php |
|||
|
|||
use yii\db\Migration; |
|||
|
|||
/** |
|||
* Class m191217_091658_update_column_is_manual_in_table_atg_goods_sku |
|||
*/ |
|||
class m191217_091658_update_column_is_manual_in_table_atg_goods_sku extends Migration |
|||
{ |
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function safeUp() |
|||
{ |
|||
$this->dropColumn('atg_goods_sku', 'is_manaul'); |
|||
$this->addColumn('atg_goods_sku', 'is_manual', $this->tinyInteger(1)->notNull()->defaultValue(0)->comment('是否手动')); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function safeDown() |
|||
{ |
|||
return true; |
|||
} |
|||
|
|||
/* |
|||
// Use up()/down() to run migration code without a transaction.
|
|||
public function up() |
|||
{ |
|||
|
|||
} |
|||
|
|||
public function down() |
|||
{ |
|||
echo "m191217_091658_update_column_is_manual_in_table_atg_goods_sku cannot be reverted.\n"; |
|||
|
|||
return false; |
|||
} |
|||
*/ |
|||
} |
@ -0,0 +1,26 @@ |
|||
<?php |
|||
|
|||
use yii\db\Migration; |
|||
|
|||
/** |
|||
* Class m191217_092101_add_column_sku_image_in_table_atg_goods_sku |
|||
*/ |
|||
class m191217_092101_add_column_sku_image_in_table_atg_goods_sku extends Migration |
|||
{ |
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function safeUp() |
|||
{ |
|||
$this->addColumn('atg_goods_sku', 'sku_image', $this->integer(11)->notNull()->defaultValue("0")->comment('sku图片id')); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function safeDown() |
|||
{ |
|||
$this->dropColumn('atg_goods_sku', 'sku_image'); |
|||
return true; |
|||
} |
|||
} |
@ -0,0 +1,175 @@ |
|||
<?php |
|||
|
|||
namespace backend\modules\shop\controllers; |
|||
|
|||
use Yii; |
|||
use backend\modules\shop\models\ars\AfterSale; |
|||
use backend\modules\shop\models\searchs\AfterSaleSearch; |
|||
use yii\web\Controller; |
|||
use yii\web\NotFoundHttpException; |
|||
use yii\filters\VerbFilter; |
|||
use iron\widget\Excel; |
|||
use yii\web\Response; |
|||
|
|||
/** |
|||
* AfterSaleController implements the CRUD actions for AfterSale model. |
|||
*/ |
|||
class AfterSaleController extends Controller |
|||
{ |
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function behaviors() |
|||
{ |
|||
return [ |
|||
'verbs' => [ |
|||
'class' => VerbFilter::className(), |
|||
'actions' => [ |
|||
'delete' => ['POST'], |
|||
], |
|||
], |
|||
]; |
|||
} |
|||
|
|||
/** |
|||
* Lists all AfterSale models. |
|||
* @return mixed |
|||
*/ |
|||
public function actionIndex() |
|||
{ |
|||
$searchModel = new AfterSaleSearch(); |
|||
$dataProvider = $searchModel->search(Yii::$app->request->queryParams); |
|||
|
|||
return $this->render('index', [ |
|||
'searchModel' => $searchModel, |
|||
'dataProvider' => $dataProvider, |
|||
'columns' => $searchModel->columns() |
|||
]); |
|||
} |
|||
|
|||
/** |
|||
* Displays a single AfterSale model. |
|||
* @param integer $id |
|||
* @return mixed |
|||
* @throws NotFoundHttpException if the model cannot be found |
|||
*/ |
|||
public function actionView($id) |
|||
{ |
|||
return $this->render('view', [ |
|||
'model' => $this->findModel($id), |
|||
]); |
|||
} |
|||
|
|||
/** |
|||
* Creates a new AfterSale model. |
|||
* If creation is successful, the browser will be redirected to the 'view' page. |
|||
* @return mixed |
|||
*/ |
|||
public function actionCreate() |
|||
{ |
|||
$model = new AfterSale(); |
|||
|
|||
if ($model->load(Yii::$app->request->post()) && $model->save()) { |
|||
return $this->redirect('index'); |
|||
} |
|||
|
|||
return $this->render('create', [ |
|||
'model' => $model, |
|||
]); |
|||
} |
|||
|
|||
/** |
|||
* Updates an existing AfterSale model. |
|||
* If update is successful, the browser will be redirected to the 'view' page. |
|||
* @param integer $id |
|||
* @return mixed |
|||
* @throws NotFoundHttpException if the model cannot be found |
|||
*/ |
|||
public function actionUpdate($id) |
|||
{ |
|||
$model = $this->findModel($id); |
|||
|
|||
if ($model->load(Yii::$app->request->post()) && $model->save()) { |
|||
return $this->redirect('index'); |
|||
} |
|||
|
|||
return $this->render('update', [ |
|||
'model' => $model, |
|||
]); |
|||
} |
|||
|
|||
/** |
|||
* Deletes an existing AfterSale model. |
|||
* If deletion is successful, the browser will be redirected to the 'index' page. |
|||
* @param integer $id |
|||
* @return mixed |
|||
* @throws NotFoundHttpException if the model cannot be found |
|||
*/ |
|||
public function actionDelete($id) |
|||
{ |
|||
$this->findModel($id); |
|||
|
|||
return $this->redirect(['index']); |
|||
} |
|||
|
|||
/** |
|||
* Finds the AfterSale model based on its primary key value. |
|||
* If the model is not found, a 404 HTTP exception will be thrown. |
|||
* @param integer $id |
|||
* @return AfterSale the loaded model |
|||
* @throws NotFoundHttpException if the model cannot be found |
|||
*/ |
|||
protected function findModel($id) |
|||
{ |
|||
if (($model = AfterSale::findOne($id)) !== null) { |
|||
return $model; |
|||
} |
|||
|
|||
throw new NotFoundHttpException('The requested page does not exist.'); |
|||
} |
|||
/** |
|||
* @author iron |
|||
* 文件导出 |
|||
*/ |
|||
public function actionExport() |
|||
{ |
|||
$searchModel = new AfterSaleSearch(); |
|||
$params = Yii::$app->request->queryParams; |
|||
if ($params['page-type'] == 'all') { |
|||
$dataProvider = $searchModel->allData($params); |
|||
} else { |
|||
$dataProvider = $searchModel->search($params); |
|||
} |
|||
Excel::export([ |
|||
'models' => $dataProvider->getModels(), |
|||
'format' => 'Xlsx', |
|||
'asAttachment' => true, |
|||
'fileName' =>'After Sales'. "-" .date('Y-m-d H/i/s', time()), |
|||
'columns' => $searchModel->columns() |
|||
]); |
|||
} |
|||
|
|||
/** |
|||
* @param $status |
|||
* @param $id |
|||
* @return Response |
|||
* 处理售后申请 |
|||
*/ |
|||
public function actionHandle($status, $id) |
|||
{ |
|||
$url = Yii::$app->request->referrer; |
|||
$model = AfterSale::findOne($id); |
|||
if ($status == AfterSale::STATUS_ACCEPT) { |
|||
$model->status = AfterSale::STATUS_ACCEPT; |
|||
} else { |
|||
$model->status = AfterSale::STATUS_REJECT; |
|||
} |
|||
$model->dealt_at = time(); |
|||
$model->operator_id = Yii::$app->user->id; |
|||
if ($model->save()) { |
|||
return $this->redirect('index'); |
|||
} else { |
|||
return $this->redirect($url); |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,179 @@ |
|||
<?php |
|||
|
|||
namespace backend\modules\shop\controllers; |
|||
|
|||
use Yii; |
|||
use backend\modules\shop\models\ars\Comment; |
|||
use backend\modules\shop\models\searchs\CommentSearch; |
|||
use yii\web\Controller; |
|||
use yii\web\NotFoundHttpException; |
|||
use yii\filters\VerbFilter; |
|||
use iron\widget\Excel; |
|||
use yii\web\Response; |
|||
|
|||
/** |
|||
* CommentController implements the CRUD actions for Comment model. |
|||
*/ |
|||
class CommentController extends Controller |
|||
{ |
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function behaviors() |
|||
{ |
|||
return [ |
|||
'verbs' => [ |
|||
'class' => VerbFilter::className(), |
|||
'actions' => [ |
|||
'delete' => ['POST'], |
|||
], |
|||
], |
|||
]; |
|||
} |
|||
|
|||
/** |
|||
* Lists all Comment models. |
|||
* @return mixed |
|||
*/ |
|||
public function actionIndex() |
|||
{ |
|||
$searchModel = new CommentSearch(); |
|||
$dataProvider = $searchModel->search(Yii::$app->request->queryParams); |
|||
|
|||
return $this->render('index', [ |
|||
'searchModel' => $searchModel, |
|||
'dataProvider' => $dataProvider, |
|||
'columns' => $searchModel->columns() |
|||
]); |
|||
} |
|||
|
|||
/** |
|||
* Displays a single Comment model. |
|||
* @param integer $id |
|||
* @return mixed |
|||
* @throws NotFoundHttpException if the model cannot be found |
|||
*/ |
|||
public function actionView($id) |
|||
{ |
|||
return $this->render('view', [ |
|||
'model' => $this->findModel($id), |
|||
]); |
|||
} |
|||
|
|||
/** |
|||
* Creates a new Comment model. |
|||
* If creation is successful, the browser will be redirected to the 'view' page. |
|||
* @return mixed |
|||
*/ |
|||
public function actionCreate() |
|||
{ |
|||
$model = new Comment(); |
|||
|
|||
if ($model->load(Yii::$app->request->post()) && $model->save()) { |
|||
return $this->redirect('index'); |
|||
} |
|||
|
|||
return $this->render('create', [ |
|||
'model' => $model, |
|||
]); |
|||
} |
|||
|
|||
/** |
|||
* Updates an existing Comment model. |
|||
* If update is successful, the browser will be redirected to the 'view' page. |
|||
* @param integer $id |
|||
* @return mixed |
|||
* @throws NotFoundHttpException if the model cannot be found |
|||
*/ |
|||
public function actionUpdate($id) |
|||
{ |
|||
$model = $this->findModel($id); |
|||
|
|||
if ($model->load(Yii::$app->request->post()) && $model->save()) { |
|||
return $this->redirect('index'); |
|||
} |
|||
|
|||
return $this->render('update', [ |
|||
'model' => $model, |
|||
]); |
|||
} |
|||
|
|||
/** |
|||
* Deletes an existing Comment model. |
|||
* If deletion is successful, the browser will be redirected to the 'index' page. |
|||
* @param integer $id |
|||
* @return mixed |
|||
* @throws NotFoundHttpException if the model cannot be found |
|||
*/ |
|||
public function actionDelete($id) |
|||
{ |
|||
$this->findModel($id)->delete(); |
|||
|
|||
return $this->redirect(['index']); |
|||
} |
|||
|
|||
/** |
|||
* Finds the Comment model based on its primary key value. |
|||
* If the model is not found, a 404 HTTP exception will be thrown. |
|||
* @param integer $id |
|||
* @return Comment the loaded model |
|||
* @throws NotFoundHttpException if the model cannot be found |
|||
*/ |
|||
protected function findModel($id) |
|||
{ |
|||
if (($model = Comment::findOne($id)) !== null) { |
|||
return $model; |
|||
} |
|||
|
|||
throw new NotFoundHttpException('The requested page does not exist.'); |
|||
} |
|||
/** |
|||
* @author iron |
|||
* 文件导出 |
|||
*/ |
|||
public function actionExport() |
|||
{ |
|||
$searchModel = new CommentSearch(); |
|||
$params = Yii::$app->request->queryParams; |
|||
if ($params['page-type'] == 'all') { |
|||
$dataProvider = $searchModel->allData($params); |
|||
} else { |
|||
$dataProvider = $searchModel->search($params); |
|||
} |
|||
Excel::export([ |
|||
'models' => $dataProvider->getModels(), |
|||
'format' => 'Xlsx', |
|||
'asAttachment' => true, |
|||
'fileName' =>'Comments'. "-" .date('Y-m-d H/i/s', time()), |
|||
'columns' => $searchModel->columns() |
|||
]); |
|||
} |
|||
|
|||
/** |
|||
* 显示评论 |
|||
* @param $id |
|||
* @return Response |
|||
* @throws NotFoundHttpException |
|||
*/ |
|||
public function actionDisplay($id) |
|||
{ |
|||
$model = $this->findModel($id); |
|||
$model->status = Comment::STATUS_DISPLAY; |
|||
$model->save(); |
|||
return $this->redirect(['index']); |
|||
} |
|||
|
|||
/** |
|||
* 隐藏评论 |
|||
* @param $id |
|||
* @return Response |
|||
* @throws NotFoundHttpException |
|||
*/ |
|||
public function actionHide($id) |
|||
{ |
|||
$model = $this->findModel($id); |
|||
$model->status = Comment::STATUS_HIDE; |
|||
$model->save(); |
|||
return $this->redirect(['index']); |
|||
} |
|||
} |
@ -0,0 +1,139 @@ |
|||
<?php |
|||
namespace backend\modules\shop\logic; |
|||
|
|||
use backend\modules\shop\models\ars\City; |
|||
use backend\modules\shop\models\ars\ExpressArea; |
|||
use backend\modules\shop\models\ars\Province; |
|||
use backend\modules\shop\models\ars\ExpressTemplate; |
|||
|
|||
class ShopManager |
|||
{ |
|||
//单位类型
|
|||
const UNIT_TYPE_WEIGHT = 1; //重量
|
|||
const UNIT_TYPE_MONEY = 2; //金额
|
|||
const UNIT_TYPE_ITEM = 3; //件
|
|||
const UNIT_TYPE_LENGTH = 4; //长度,宽度,高度,直径
|
|||
|
|||
/** |
|||
* @param $type |
|||
* @return int |
|||
* 比例转换 |
|||
*/ |
|||
public static function proportionalConversion($type) |
|||
{ |
|||
switch ($type) { |
|||
case self::UNIT_TYPE_WEIGHT: |
|||
return 1000; //后台显示为kg,数据库保存为g
|
|||
break; |
|||
case self::UNIT_TYPE_MONEY: |
|||
return 100; //后台显示为元,数据库保存为分
|
|||
break; |
|||
case self::UNIT_TYPE_ITEM: |
|||
return 1; //后台显示和数据库都为件
|
|||
break; |
|||
case self::UNIT_TYPE_LENGTH: |
|||
return 1; //后台和数据库都为毫米
|
|||
break; |
|||
default: |
|||
return 1; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* @param ExpressArea|$expressAreaModel |
|||
* @param ExpressTemplate|$expressTemplateModel |
|||
* 区域运费模板按比例转化数据 |
|||
*/ |
|||
public static function expressAreaScaleDate($expressAreaModel, $expressTemplateModel) |
|||
{ |
|||
$expressAreaModel->basic_price *= self::proportionalConversion(self::UNIT_TYPE_MONEY); |
|||
$expressAreaModel->extra_price *= self::proportionalConversion(self::UNIT_TYPE_MONEY); |
|||
if ($expressTemplateModel->calculation_type == ExpressTemplate::CALCULATION_TYPE_WEIGHT) { |
|||
$expressAreaModel->basic_count *= self::proportionalConversion(self::UNIT_TYPE_WEIGHT); |
|||
$expressAreaModel->extra_count *= self::proportionalConversion(self::UNIT_TYPE_WEIGHT); |
|||
} else { |
|||
$expressAreaModel->basic_count *= self::proportionalConversion(self::UNIT_TYPE_ITEM); |
|||
$expressAreaModel->extra_count *= self::proportionalConversion(self::UNIT_TYPE_ITEM); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* @param ExpressArea|$expressAreaModel |
|||
* @return array |
|||
* 除去已被选取的城市,筛选剩下的城市数据 |
|||
*/ |
|||
public static function filterCity($expressAreaModel) |
|||
{ |
|||
$query = ExpressArea::find() |
|||
->select(['city']) |
|||
->where(['express_template' => $expressAreaModel->express_template]); |
|||
if ($expressAreaModel->id) { //修改操作时,除去自身的城市
|
|||
$query = $query->andWhere(['!=', 'id', $expressAreaModel->id]); |
|||
} |
|||
$allDate = $query->all(); |
|||
|
|||
$expressAresCityIdArr = self::filterSelectedCtiyIdArr($allDate); |
|||
|
|||
$leftoverCityArr = self::filterLeftoverCityArr($expressAresCityIdArr); |
|||
return $leftoverCityArr; |
|||
} |
|||
|
|||
/** |
|||
* @param $expressAreas |
|||
* @return array 已选的城市id数组 |
|||
* 获取已选的城市id数组 |
|||
*/ |
|||
private static function filterSelectedCtiyIdArr($expressAreas) |
|||
{ |
|||
$expressAresCityIdArr = []; |
|||
if ($expressAreas) { |
|||
foreach ($expressAreas as $expressAreaCity) { |
|||
$cityIdArr = explode(',', $expressAreaCity->city); |
|||
$expressAresCityIdArr = array_unique(array_merge($cityIdArr, $expressAresCityIdArr)); |
|||
} |
|||
} |
|||
return $expressAresCityIdArr; |
|||
} |
|||
|
|||
/** |
|||
* @param $expressAresCityIdArr |
|||
* @return array 未选的城市id数组 |
|||
* 筛选剩下的城市 |
|||
*/ |
|||
private static function filterLeftoverCityArr($expressAresCityIdArr) |
|||
{ |
|||
$data = []; |
|||
$provinces = Province::find()->cache(0)->all(); |
|||
foreach ($provinces as $key => $province) { |
|||
$cities = City::find() |
|||
->where(['province_id' => $province->province_id]) |
|||
->andWhere(['not in', 'city_id', $expressAresCityIdArr]) |
|||
->all(); |
|||
|
|||
if ($cities) { |
|||
$data[$key]['province'] = $province->name; |
|||
foreach ($cities as $city) { |
|||
$data[$key]['city'][] = ['id' => $city->city_id, 'name' => $city->name]; |
|||
} |
|||
} |
|||
} |
|||
return array_values($data); |
|||
} |
|||
|
|||
/** |
|||
* 处理所选区域数据 |
|||
* @param $area |
|||
* @param ExpressArea|$expressAreaModel |
|||
* @param ExpressTemplate|$expressTemplateModel |
|||
* @return bool |
|||
*/ |
|||
public static function dealAreaInExpressArea($area, $expressAreaModel, $expressTemplateModel) |
|||
{ |
|||
$cityIds = array_keys($area); |
|||
$data['city'] = implode(',', $cityIds); |
|||
$expressAreaModel->load($data, ''); |
|||
self::expressAreaScaleDate($expressAreaModel, $expressTemplateModel); //按比例转换数据
|
|||
$expressAreaModel->save(); |
|||
return true; |
|||
} |
|||
} |
@ -0,0 +1,68 @@ |
|||
<?php |
|||
|
|||
use yii\db\Migration; |
|||
|
|||
/** |
|||
* Class m191209_080245_update_table_after_sale |
|||
*/ |
|||
class m191209_080245_update_table_after_sale extends Migration |
|||
{ |
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function up() |
|||
{ |
|||
$this->dropTable('ats_after_sale'); |
|||
$tableOptions = 'CHARACTER SET utf8 COLLATE utf8_general_ci ENGINE=InnoDB COMMENT="售后表"'; |
|||
$this->createTable('ats_after_sale', [ |
|||
'id' => $this->primaryKey(), |
|||
'wx_refund_id'=>$this->string(64)->defaultValue('')->notNull()->comment('微信退款单号'), |
|||
'after_sale_sn'=>$this->string(64)->defaultValue('')->notNull()->comment('售后单号'), |
|||
'user_id'=>$this->integer(11)->defaultValue('0')->notNull()->comment('用户id'), |
|||
'order_goods_id'=>$this->integer(11)->defaultValue('0')->notNull()->comment('订单商品id'), |
|||
'amount'=>$this->integer(20)->defaultValue("0")->notNull()->comment('退货时实际退的金额'), |
|||
'count'=>$this->integer(11)->defaultValue("0")->notNull()->comment('退换货的商品数量'), |
|||
'apply_at'=>$this->integer(11)->defaultValue(null)->comment('申请时间'), |
|||
'dealt_at'=>$this->integer(11)->defaultValue(null)->comment('处理时间'), |
|||
'finish_at'=>$this->integer(11)->defaultValue(null)->comment('完成时间'), |
|||
'operator_id'=>$this->integer(11)->defaultValue("0")->notNull()->comment('操作者'), |
|||
'refund_type'=>$this->tinyInteger(1)->defaultValue("1")->comment('退款类型:1:全额退款;2:部分退款'), |
|||
'description'=>$this->text()->comment('描述'), |
|||
'image' => $this->text()->comment('图片'), |
|||
'status' => $this->tinyInteger(2)->defaultValue('0')->notNull()->comment('处理状态:0:未处理;1:已同意,待买家确认;2:用户已确认;3:已拒绝;4:退款成功;5:已取消;'), |
|||
'reason'=>$this->smallInteger(2)->defaultValue("0")->comment('退换货理由'), |
|||
'remarks'=>$this->text()->comment('店家备注'), |
|||
'take_shipping_sn'=>$this->string(50)->defaultValue(null)->comment('用户发货物流单号'), |
|||
'refund_mode' => $this->tinyInteger(2)->defaultValue('1')->notNull()->comment('退款方式:1:仅退款;2:退货退款;') |
|||
],$tableOptions); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function down() |
|||
{ |
|||
$this->dropTable('ats_after_sale'); |
|||
$tableOptions = 'CHARACTER SET utf8 COLLATE utf8_general_ci ENGINE=InnoDB COMMENT="售后表"'; |
|||
$this->createTable('ats_after_sale', [ |
|||
'id' => $this->primaryKey(), |
|||
'operator_id'=>$this->integer(11)->notNull()->comment('操作者'), |
|||
'user_id'=>$this->integer(11)->notNull()->comment('用户id'), |
|||
'wx_refund_id'=>$this->string(64)->defaultValue(null)->comment('微信退款单号'), |
|||
'after_sale_sn'=>$this->string(64)->defaultValue(null)->comment('售后单号'), |
|||
'order_goods_id'=>$this->integer(11)->defaultValue(null)->comment('订单商品id'), |
|||
'count'=>$this->integer(11)->defaultValue(null)->comment('退换货的商品数量'), |
|||
'amount'=>$this->integer(20)->notNull()->comment('退货时实际退的金额'), |
|||
'type'=>$this->tinyInteger(1)->defaultValue(0)->comment('类型'), |
|||
'reason'=>$this->smallInteger(2)->defaultValue(0)->comment('退换货理由'), |
|||
'description'=>$this->text()->comment('描述'), |
|||
'take_shipping_sn'=>$this->string(50)->defaultValue(null)->comment('用户发货物流单号'), |
|||
'send_shipping_sn'=>$this->string(50)->defaultValue(null)->comment('换货商家发货的物流单号'), |
|||
'remarks'=>$this->text()->comment('店家备注'), |
|||
'applyed_at'=>$this->integer(11)->defaultValue(null)->comment('申请时间'), |
|||
'dealed_at'=>$this->integer(11)->defaultValue(null)->comment('处理时间'), |
|||
'finished_at'=>$this->integer(11)->defaultValue(null)->comment('完成时间'), |
|||
],$tableOptions); |
|||
return true; |
|||
} |
|||
} |
@ -0,0 +1,20 @@ |
|||
<?php |
|||
|
|||
use yii\db\Migration; |
|||
|
|||
/** |
|||
* Class m191217_013625_add_column_sku_value_in_table_ats_cart |
|||
*/ |
|||
class m191217_013625_add_column_sku_value_in_table_ats_cart extends Migration |
|||
{ |
|||
public function up() |
|||
{ |
|||
$this->addColumn('ats_cart', 'sku_value', $this->string(120)->defaultValue('')->comment('商品sku')); |
|||
} |
|||
|
|||
public function down() |
|||
{ |
|||
$this->dropColumn('ats_cart', 'sku_value'); |
|||
return true; |
|||
} |
|||
} |
@ -0,0 +1,20 @@ |
|||
<?php |
|||
|
|||
use yii\db\Migration; |
|||
|
|||
/** |
|||
* Class m191217_021455_update_column_star_in_table_ats_comment |
|||
*/ |
|||
class m191217_021455_update_column_star_in_table_ats_comment extends Migration |
|||
{ |
|||
public function up() |
|||
{ |
|||
$this->dropColumn('ats_comment', 'star'); |
|||
$this->addColumn('ats_comment', 'star', $this->integer(11)->notNull()->defaultValue("0")->comment('星级')); |
|||
} |
|||
|
|||
public function down() |
|||
{ |
|||
return true; |
|||
} |
|||
} |
@ -0,0 +1,188 @@ |
|||
<?php |
|||
|
|||
namespace backend\modules\shop\models\searchs; |
|||
|
|||
use yii\base\Model; |
|||
use yii\data\ActiveDataProvider; |
|||
use yii\helpers\ArrayHelper; |
|||
use backend\modules\shop\models\ars\AfterSale; |
|||
use backend\modules\shop\models\ars\OrderGoods; |
|||
|
|||
/** |
|||
* AfterSaleSearch represents the model behind the search form of `backend\modules\shop\models\ars\AfterSale`. |
|||
*/ |
|||
class AfterSaleSearch extends AfterSale |
|||
{ |
|||
/** |
|||
* @return array |
|||
* 增加创建时间查询字段 |
|||
*/ |
|||
public function attributes() |
|||
{ |
|||
return ArrayHelper::merge(['created_at_range'], parent::attributes()); |
|||
} |
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function rules() |
|||
{ |
|||
return [ |
|||
[['id', 'user_id', 'order_goods_id', 'amount', 'count', 'apply_at', 'dealt_at', 'finish_at', 'operator_id', 'refund_type', 'status', 'reason', 'refund_mode'], 'integer'], |
|||
[['wx_refund_id', 'after_sale_sn', 'description', 'image', 'remarks', 'take_shipping_sn'], 'safe'], |
|||
['created_at_range','safe'], |
|||
]; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function scenarios() |
|||
{ |
|||
// bypass scenarios() implementation in the parent class
|
|||
return Model::scenarios(); |
|||
} |
|||
/** |
|||
* @return array |
|||
* 列格式 |
|||
*/ |
|||
public function columns() |
|||
{ |
|||
return [ |
|||
[ |
|||
'class' => 'blobt\grid\CheckboxColumn', |
|||
'width' => '2%', |
|||
'align' => 'center' |
|||
], |
|||
'id', |
|||
'after_sale_sn', |
|||
[ |
|||
'attribute' => 'order_goods_id', |
|||
'value' => function ($model) { |
|||
return $model->goods->goods_name; |
|||
} |
|||
], |
|||
[ |
|||
'attribute' => 'order_pay_amount', |
|||
'value' => function ($model) { |
|||
return sprintf("%1\$.2f", $model->goods->order->payment_amount); |
|||
}, |
|||
'label' => '订单支付金额' |
|||
], |
|||
[ |
|||
'attribute' => 'amount', |
|||
'value' => function ($model) { |
|||
return sprintf("%1\$.2f", $model->amount); |
|||
} |
|||
], |
|||
[ |
|||
'attribute' => 'status', |
|||
'value' => function ($model) { |
|||
return AfterSale::$status[$model->status]; |
|||
} |
|||
], |
|||
'apply_at:datetime', |
|||
'dealt_at:datetime', |
|||
'finish_at:datetime', |
|||
[ |
|||
'class' => 'iron\grid\ActionColumn', |
|||
'align' => 'center', |
|||
'config' => [ |
|||
[ |
|||
'name' => 'view', |
|||
'icon' => 'list', |
|||
'title' => '详情', |
|||
] |
|||
] |
|||
], |
|||
]; |
|||
} |
|||
/** |
|||
* @param $params |
|||
* @return ActiveDataProvider |
|||
* 不分页的所有数据 |
|||
*/ |
|||
public function allData($params) |
|||
{ |
|||
$query = AfterSale::find(); |
|||
$dataProvider = new ActiveDataProvider([ |
|||
'query' => $query, |
|||
'pagination' => false, |
|||
'sort' => false |
|||
]); |
|||
$this->load($params); |
|||
return $this->filter($query, $dataProvider); |
|||
} |
|||
|
|||
/** |
|||
* Creates data provider instance with search query applied |
|||
* |
|||
* @param array $params |
|||
* |
|||
* @return ActiveDataProvider |
|||
*/ |
|||
public function search($params) |
|||
{ |
|||
$query = AfterSale::find(); |
|||
|
|||
// add conditions that should always apply here
|
|||
|
|||
$dataProvider = new ActiveDataProvider([ |
|||
'query' => $query, |
|||
'pagination' => [ |
|||
'pageSizeLimit' => [1, 200] |
|||
], |
|||
'sort' => [ |
|||
'defaultOrder' => [ |
|||
'id' => SORT_DESC, |
|||
] |
|||
], |
|||
]); |
|||
|
|||
$this->load($params); |
|||
return $this->filter($query, $dataProvider); |
|||
} |
|||
/** |
|||
* @param $query |
|||
* @param $dataProvider |
|||
* @return ActiveDataProvider |
|||
* 条件筛选 |
|||
*/ |
|||
private function filter($query, $dataProvider){ |
|||
if (!$this->validate()) { |
|||
// uncomment the following line if you do not want to return any records when validation fails
|
|||
// $query->where('0=1');
|
|||
return $dataProvider; |
|||
} |
|||
|
|||
// grid filtering conditions
|
|||
$query->andFilterWhere([ |
|||
'id' => $this->id, |
|||
'user_id' => $this->user_id, |
|||
'order_goods_id' => $this->order_goods_id, |
|||
'amount' => $this->amount, |
|||
'count' => $this->count, |
|||
'apply_at' => $this->apply_at, |
|||
'dealt_at' => $this->dealt_at, |
|||
'finish_at' => $this->finish_at, |
|||
'operator_id' => $this->operator_id, |
|||
'refund_type' => $this->refund_type, |
|||
'status' => $this->status, |
|||
'reason' => $this->reason, |
|||
'refund_mode' => $this->refund_mode, |
|||
]); |
|||
|
|||
$query->andFilterWhere(['like', 'wx_refund_id', $this->wx_refund_id]) |
|||
->andFilterWhere(['like', 'after_sale_sn', $this->after_sale_sn]) |
|||
->andFilterWhere(['like', 'description', $this->description]) |
|||
->andFilterWhere(['like', 'image', $this->image]) |
|||
->andFilterWhere(['like', 'remarks', $this->remarks]) |
|||
->andFilterWhere(['like', 'take_shipping_sn', $this->take_shipping_sn]); |
|||
if ($this->created_at_range) { |
|||
$arr = explode(' ~ ', $this->created_at_range); |
|||
$start = strtotime($arr[0]); |
|||
$end = strtotime($arr[1]) + 3600 * 24; |
|||
$query->andFilterWhere(['between', 'created_at', $start, $end]); |
|||
} |
|||
return $dataProvider; |
|||
} |
|||
} |
@ -0,0 +1,186 @@ |
|||
<?php |
|||
|
|||
namespace backend\modules\shop\models\searchs; |
|||
|
|||
use yii\base\Model; |
|||
use yii\data\ActiveDataProvider; |
|||
use yii\helpers\ArrayHelper; |
|||
use backend\modules\shop\models\ars\Comment; |
|||
|
|||
/** |
|||
* CommentSearch represents the model behind the search form of `backend\modules\shop\models\ars\Comment`. |
|||
*/ |
|||
class CommentSearch extends Comment |
|||
{ |
|||
/** |
|||
* @return array |
|||
* 增加创建时间查询字段 |
|||
*/ |
|||
public function attributes() |
|||
{ |
|||
return ArrayHelper::merge(['created_at_range'], parent::attributes()); |
|||
} |
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function rules() |
|||
{ |
|||
return [ |
|||
[['id', 'user_id', 'order_goods_id', 'star', 'status', 'updated_at', 'created_at'], 'integer'], |
|||
[['content'], 'safe'], |
|||
['created_at_range','safe'], |
|||
]; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function scenarios() |
|||
{ |
|||
// bypass scenarios() implementation in the parent class
|
|||
return Model::scenarios(); |
|||
} |
|||
/** |
|||
* @return array |
|||
* 列格式 |
|||
*/ |
|||
public function columns() |
|||
{ |
|||
return [ |
|||
'id', |
|||
'user_id', |
|||
[ |
|||
'attribute' => 'order_goods_id', |
|||
'value' => function ($model) { |
|||
return $model->orderGoods->goods_name; |
|||
}, |
|||
'label' => '商品名称' |
|||
], |
|||
'star', |
|||
'content', |
|||
[ |
|||
'attribute' => 'status', |
|||
'value' => function ($model) { |
|||
return Comment::$commentStatus[$model->status]; |
|||
}, |
|||
'label' => '状态' |
|||
], |
|||
'created_at:datetime', |
|||
[ |
|||
'class' => 'iron\grid\ActionColumn', |
|||
'align' => 'center', |
|||
'config' => [ |
|||
[ |
|||
'name' => 'view', |
|||
'icon' => 'list', |
|||
'title' => '详情', |
|||
], |
|||
[ |
|||
'name' => 'display', |
|||
'icon' => 'eye', |
|||
'title' => '显示', |
|||
'hide' => [ |
|||
'attributes' => [ |
|||
'status' |
|||
], |
|||
'values' => [ |
|||
Comment::STATUS_DISPLAY |
|||
] |
|||
] |
|||
], |
|||
[ |
|||
'name' => 'hide', |
|||
'icon' => 'eye', |
|||
'title' => '隐藏', |
|||
'hide' => [ |
|||
'attributes' => [ |
|||
'status' |
|||
], |
|||
'values' => [ |
|||
Comment::STATUS_HIDE |
|||
] |
|||
] |
|||
], |
|||
] |
|||
], |
|||
]; |
|||
} |
|||
/** |
|||
* @param $params |
|||
* @return ActiveDataProvider |
|||
* 不分页的所有数据 |
|||
*/ |
|||
public function allData($params) |
|||
{ |
|||
$query = Comment::find(); |
|||
$dataProvider = new ActiveDataProvider([ |
|||
'query' => $query, |
|||
'pagination' => false, |
|||
'sort' => false |
|||
]); |
|||
$this->load($params); |
|||
return $this->filter($query, $dataProvider); |
|||
} |
|||
|
|||
/** |
|||
* Creates data provider instance with search query applied |
|||
* |
|||
* @param array $params |
|||
* |
|||
* @return ActiveDataProvider |
|||
*/ |
|||
public function search($params) |
|||
{ |
|||
$query = Comment::find(); |
|||
|
|||
// add conditions that should always apply here
|
|||
|
|||
$dataProvider = new ActiveDataProvider([ |
|||
'query' => $query, |
|||
'pagination' => [ |
|||
'pageSizeLimit' => [1, 200] |
|||
], |
|||
'sort' => [ |
|||
'defaultOrder' => [ |
|||
'id' => SORT_DESC, |
|||
] |
|||
], |
|||
]); |
|||
|
|||
$this->load($params); |
|||
return $this->filter($query, $dataProvider); |
|||
} |
|||
/** |
|||
* @param $query |
|||
* @param $dataProvider |
|||
* @return ActiveDataProvider |
|||
* 条件筛选 |
|||
*/ |
|||
private function filter($query, $dataProvider){ |
|||
if (!$this->validate()) { |
|||
// uncomment the following line if you do not want to return any records when validation fails
|
|||
// $query->where('0=1');
|
|||
return $dataProvider; |
|||
} |
|||
|
|||
// grid filtering conditions
|
|||
$query->andFilterWhere([ |
|||
'id' => $this->id, |
|||
'user_id' => $this->user_id, |
|||
'order_goods_id' => $this->order_goods_id, |
|||
'star' => $this->star, |
|||
'status' => $this->status, |
|||
'updated_at' => $this->updated_at, |
|||
'created_at' => $this->created_at, |
|||
]); |
|||
|
|||
$query->andFilterWhere(['like', 'content', $this->content]); |
|||
if ($this->created_at_range) { |
|||
$arr = explode(' ~ ', $this->created_at_range); |
|||
$start = strtotime($arr[0]); |
|||
$end = strtotime($arr[1]) + 3600 * 24; |
|||
$query->andFilterWhere(['between', 'created_at', $start, $end]); |
|||
} |
|||
return $dataProvider; |
|||
} |
|||
} |
@ -0,0 +1,58 @@ |
|||
<?php |
|||
|
|||
use yii\helpers\Html; |
|||
use yii\bootstrap4\ActiveForm; |
|||
|
|||
/* @var $this yii\web\View */ |
|||
/* @var $model backend\modules\shop\models\ars\AfterSale */ |
|||
/* @var $form yii\widgets\ActiveForm */ |
|||
?>
|
|||
|
|||
<div class="after-sale-form"> |
|||
|
|||
<?php $form = ActiveForm::begin(); ?>
|
|||
|
|||
<?= $form->field($model, 'wx_refund_id')->textInput(['maxlength' => true]) ?>
|
|||
|
|||
<?= $form->field($model, 'after_sale_sn')->textInput(['maxlength' => true]) ?>
|
|||
|
|||
<?= $form->field($model, 'user_id')->textInput() ?>
|
|||
|
|||
<?= $form->field($model, 'order_goods_id')->textInput() ?>
|
|||
|
|||
<?= $form->field($model, 'amount')->textInput() ?>
|
|||
|
|||
<?= $form->field($model, 'count')->textInput() ?>
|
|||
|
|||
<?= $form->field($model, 'apply_at')->textInput() ?>
|
|||
|
|||
<?= $form->field($model, 'dealt_at')->textInput() ?>
|
|||
|
|||
<?= $form->field($model, 'finish_at')->textInput() ?>
|
|||
|
|||
<?= $form->field($model, 'operator_id')->textInput() ?>
|
|||
|
|||
<?= $form->field($model, 'refund_type')->textInput() ?>
|
|||
|
|||
<?= $form->field($model, 'description')->textarea(['rows' => 6]) ?>
|
|||
|
|||
<?= $form->field($model, 'image')->textarea(['rows' => 6]) ?>
|
|||
|
|||
<?= $form->field($model, 'status')->textInput() ?>
|
|||
|
|||
<?= $form->field($model, 'reason')->textInput() ?>
|
|||
|
|||
<?= $form->field($model, 'remarks')->textarea(['rows' => 6]) ?>
|
|||
|
|||
<?= $form->field($model, 'take_shipping_sn')->textInput(['maxlength' => true]) ?>
|
|||
|
|||
<?= $form->field($model, 'refund_mode')->textInput() ?>
|
|||
|
|||
<div class="form-group"> |
|||
<?= Html::submitButton('保存', ['class' => 'btn btn-success']) ?>
|
|||
<?= Html::a('返回', ['index'], ['class' => 'btn btn-info']) ?>
|
|||
</div> |
|||
|
|||
<?php ActiveForm::end(); ?>
|
|||
|
|||
</div> |
@ -0,0 +1,62 @@ |
|||
<?php |
|||
|
|||
use yii\helpers\Html; |
|||
use yii\widgets\ActiveForm; |
|||
use \blobt\widgets\DateRangePicker; |
|||
|
|||
/* @var $this yii\web\View */ |
|||
/* @var $model backend\modules\shop\models\searchs\AfterSaleSearch */ |
|||
/* @var $form yii\widgets\ActiveForm */ |
|||
?>
|
|||
|
|||
<?php $form = ActiveForm::begin([ |
|||
'action' => ['index'], |
|||
'method' => 'get', |
|||
'validateOnType' => true, |
|||
]); |
|||
?>
|
|||
<div class="row"> |
|||
<div class="col"> |
|||
<?= $form->field($model, 'id', [ |
|||
"template" => "{input}{error}", |
|||
"inputOptions" => [ |
|||
"placeholder" => "检索ID", |
|||
"class" => "form-control", |
|||
], |
|||
"errorOptions" => [ |
|||
"class" => "error-tips" |
|||
] |
|||
]) |
|||
?>
|
|||
</div> |
|||
<div class="col"> |
|||
<?= $form->field($model, 'after_sale_sn', [ |
|||
"template" => "{input}{error}", |
|||
"inputOptions" => [ |
|||
"placeholder" => "售后单号", |
|||
"class" => "form-control", |
|||
], |
|||
"errorOptions" => [ |
|||
"class" => "error-tips" |
|||
] |
|||
]) |
|||
?>
|
|||
</div> |
|||
<div class="col"> |
|||
<?= $form->field($model, "created_at_range", [ |
|||
"template" => "{input}{error}", |
|||
"inputOptions" => [ |
|||
"placeholder" => "创建时间", |
|||
], |
|||
"errorOptions" => [ |
|||
"class" => "error-tips" |
|||
] |
|||
])->widget(DateRangePicker::className()); |
|||
?>
|
|||
</div> |
|||
<div class="form-group"> |
|||
<?= Html::submitButton('<i class="fa fa-filter"></i>', ['class' => 'btn btn-default']) ?>
|
|||
<?= Html::resetButton('<i class="fa fa-eraser"></i>', ['class' => 'btn btn-default']) ?>
|
|||
</div> |
|||
</div> |
|||
<?php ActiveForm::end(); ?>
|
@ -0,0 +1,18 @@ |
|||
<?php |
|||
|
|||
use yii\helpers\Html; |
|||
|
|||
/* @var $this yii\web\View */ |
|||
/* @var $model backend\modules\shop\models\ars\AfterSale */ |
|||
|
|||
$this->title = '创建 After Sale'; |
|||
$this->params['breadcrumbs'][] = ['label' => 'After Sales', 'url' => ['index']]; |
|||
$this->params['breadcrumbs'][] = $this->title; |
|||
?>
|
|||
<div class="after-sale-create"> |
|||
|
|||
<?= $this->render('_form', [ |
|||
'model' => $model, |
|||
]) ?>
|
|||
|
|||
</div> |
@ -0,0 +1,31 @@ |
|||
<?php |
|||
|
|||
use yii\helpers\Html; |
|||
use iron\grid\GridView; |
|||
|
|||
/* @var $this yii\web\View */ |
|||
/* @var $searchModel backend\modules\shop\models\searchs\AfterSaleSearch */ |
|||
/* @var $dataProvider yii\data\ActiveDataProvider */ |
|||
|
|||
$this->title = '售后管理'; |
|||
$this->params['breadcrumbs'][] = $this->title; |
|||
?>
|
|||
<div class="row"> |
|||
<div class="col-12"> |
|||
<?= GridView::widget([ |
|||
'dataProvider' => $dataProvider, |
|||
'filter' => $this->render("_search", ['model' => $searchModel]), |
|||
'batch' => [ |
|||
[ |
|||
"label" => "删除", |
|||
"url" => "after-sale/deletes" |
|||
], |
|||
], |
|||
'columns' => $columns, |
|||
'batchTemplate' => '', |
|||
'export' => '', |
|||
'create' => '' |
|||
]); |
|||
?>
|
|||
</div> |
|||
</div> |
@ -0,0 +1,19 @@ |
|||
<?php |
|||
|
|||
use yii\helpers\Html; |
|||
|
|||
/* @var $this yii\web\View */ |
|||
/* @var $model backend\modules\shop\models\ars\AfterSale */ |
|||
|
|||
$this->title = '编辑 After Sale: ' . $model->id; |
|||
$this->params['breadcrumbs'][] = ['label' => 'After Sales', 'url' => ['index']]; |
|||
$this->params['breadcrumbs'][] = ['label' => $model->id, 'url' => ['view', 'id' => $model->id]]; |
|||
$this->params['breadcrumbs'][] = 'Update '; |
|||
?>
|
|||
<div class="after-sale-update"> |
|||
|
|||
<?= $this->render('_form', [ |
|||
'model' => $model, |
|||
]) ?>
|
|||
|
|||
</div> |
@ -0,0 +1,108 @@ |
|||
<?php |
|||
|
|||
use yii\helpers\Html; |
|||
use yii\widgets\DetailView; |
|||
use backend\modules\shop\models\ars\AfterSale; |
|||
|
|||
/* @var $this yii\web\View */ |
|||
/* @var $model backend\modules\shop\models\ars\AfterSale */ |
|||
|
|||
$this->title = $model->id; |
|||
$this->params['breadcrumbs'][] = ['label' => 'After Sales', 'url' => ['index']]; |
|||
$this->params['breadcrumbs'][] = $this->title; |
|||
\yii\web\YiiAsset::register($this); |
|||
?>
|
|||
<div class="after-sale-view"> |
|||
|
|||
<p> |
|||
<?= Html::a('返回列表', ['index'], ['class' => 'btn btn-default']) ?>
|
|||
<?php |
|||
if ($model->status == AfterSale::STATUS_UNTREATED) { |
|||
echo Html::a('同意', |
|||
[ |
|||
'handle', |
|||
'status' => AfterSale::STATUS_ACCEPT, |
|||
'id' => $model->id |
|||
], |
|||
[ |
|||
'class' => 'btn btn-default', |
|||
'data' => [ |
|||
'confirm' => '同意用户的退货申请?', |
|||
'method' => 'post', |
|||
] |
|||
] |
|||
); |
|||
echo Html::a('拒绝', |
|||
[ |
|||
'handle', |
|||
'status' => AfterSale::STATUS_REJECT, |
|||
'id' => $model->id |
|||
], |
|||
[ |
|||
'class' => 'btn btn-default', |
|||
'data' => [ |
|||
'confirm' => '拒绝用户的退货申请?', |
|||
'method' => 'post', |
|||
], |
|||
] |
|||
); |
|||
} |
|||
?>
|
|||
</p> |
|||
|
|||
<?= DetailView::widget([ |
|||
'model' => $model, |
|||
'attributes' => [ |
|||
'id', |
|||
'wx_refund_id', |
|||
'after_sale_sn', |
|||
'user_id', |
|||
[ |
|||
'attribute' => 'order_goods_id', |
|||
'value' => function ($model) { |
|||
return $model->goods->goods_name; |
|||
} |
|||
], |
|||
[ |
|||
'attribute' => 'amount', |
|||
'value' => function ($model) { |
|||
return sprintf("%1\$.2f", $model->amount); |
|||
} |
|||
], |
|||
'count', |
|||
'apply_at:datetime', |
|||
'dealt_at:datetime', |
|||
'finish_at:datetime', |
|||
'operator_id', |
|||
[ |
|||
'attribute' => 'refund_type', |
|||
'value' => function ($model) { |
|||
return AfterSale::$refundType[$model->refund_type]; |
|||
} |
|||
], |
|||
'description:ntext', |
|||
'image:ntext', |
|||
[ |
|||
'attribute' => 'status', |
|||
'value' => function ($model) { |
|||
return AfterSale::$status[$model->status]; |
|||
} |
|||
], |
|||
[ |
|||
'attribute' => 'reason', |
|||
'value' => function ($model) { |
|||
return $model->reason ? AfterSale::$afterSaleReason[$model->reason] : '未设置'; |
|||
} |
|||
], |
|||
'remarks:ntext', |
|||
'take_shipping_sn', |
|||
[ |
|||
'attribute' => 'refund_mode', |
|||
'value' => function ($model) { |
|||
return AfterSale::$refundMode[$model->refund_mode]; |
|||
} |
|||
], |
|||
], |
|||
]) ?>
|
|||
|
|||
</div> |
@ -0,0 +1,32 @@ |
|||
<?php |
|||
|
|||
use yii\helpers\Html; |
|||
use yii\bootstrap4\ActiveForm; |
|||
|
|||
/* @var $this yii\web\View */ |
|||
/* @var $model backend\modules\shop\models\ars\Comment */ |
|||
/* @var $form yii\widgets\ActiveForm */ |
|||
?>
|
|||
|
|||
<div class="comment-form"> |
|||
|
|||
<?php $form = ActiveForm::begin(); ?>
|
|||
|
|||
<?= $form->field($model, 'user_id')->textInput() ?>
|
|||
|
|||
<?= $form->field($model, 'order_goods_id')->textInput() ?>
|
|||
|
|||
<?= $form->field($model, 'star')->textInput() ?>
|
|||
|
|||
<?= $form->field($model, 'content')->textarea(['rows' => 6]) ?>
|
|||
|
|||
<?= $form->field($model, 'status')->textInput() ?>
|
|||
|
|||
<div class="form-group"> |
|||
<?= Html::submitButton('保存', ['class' => 'btn btn-success']) ?>
|
|||
<?= Html::a('返回', ['index'], ['class' => 'btn btn-info']) ?>
|
|||
</div> |
|||
|
|||
<?php ActiveForm::end(); ?>
|
|||
|
|||
</div> |
@ -0,0 +1,49 @@ |
|||
<?php |
|||
|
|||
use yii\helpers\Html; |
|||
use yii\widgets\ActiveForm; |
|||
use \blobt\widgets\DateRangePicker; |
|||
|
|||
/* @var $this yii\web\View */ |
|||
/* @var $model backend\modules\shop\models\searchs\CommentSearch */ |
|||
/* @var $form yii\widgets\ActiveForm */ |
|||
?>
|
|||
|
|||
<?php $form = ActiveForm::begin([ |
|||
'action' => ['index'], |
|||
'method' => 'get', |
|||
'validateOnType' => true, |
|||
]); |
|||
?>
|
|||
<div class="row"> |
|||
<div class="col"> |
|||
<?= $form->field($model, 'id', [ |
|||
"template" => "{input}{error}", |
|||
"inputOptions" => [ |
|||
"placeholder" => "检索ID", |
|||
"class" => "form-control", |
|||
], |
|||
"errorOptions" => [ |
|||
"class" => "error-tips" |
|||
] |
|||
]) |
|||
?>
|
|||
</div> |
|||
<div class="col"> |
|||
<?= $form->field($model, "created_at_range", [ |
|||
"template" => "{input}{error}", |
|||
"inputOptions" => [ |
|||
"placeholder" => "创建时间", |
|||
], |
|||
"errorOptions" => [ |
|||
"class" => "error-tips" |
|||
] |
|||
])->widget(DateRangePicker::className()); |
|||
?>
|
|||
</div> |
|||
<div class="form-group"> |
|||
<?= Html::submitButton('<i class="fa fa-filter"></i>', ['class' => 'btn btn-default']) ?>
|
|||
<?= Html::resetButton('<i class="fa fa-eraser"></i>', ['class' => 'btn btn-default']) ?>
|
|||
</div> |
|||
</div> |
|||
<?php ActiveForm::end(); ?>
|
@ -0,0 +1,18 @@ |
|||
<?php |
|||
|
|||
use yii\helpers\Html; |
|||
|
|||
/* @var $this yii\web\View */ |
|||
/* @var $model backend\modules\shop\models\ars\Comment */ |
|||
|
|||
$this->title = '创建 Comment'; |
|||
$this->params['breadcrumbs'][] = ['label' => 'Comments', 'url' => ['index']]; |
|||
$this->params['breadcrumbs'][] = $this->title; |
|||
?>
|
|||
<div class="comment-create"> |
|||
|
|||
<?= $this->render('_form', [ |
|||
'model' => $model, |
|||
]) ?>
|
|||
|
|||
</div> |
@ -0,0 +1,31 @@ |
|||
<?php |
|||
|
|||
use yii\helpers\Html; |
|||
use iron\grid\GridView; |
|||
|
|||
/* @var $this yii\web\View */ |
|||
/* @var $searchModel backend\modules\shop\models\searchs\CommentSearch */ |
|||
/* @var $dataProvider yii\data\ActiveDataProvider */ |
|||
|
|||
$this->title = '评论'; |
|||
$this->params['breadcrumbs'][] = $this->title; |
|||
?>
|
|||
<div class="row"> |
|||
<div class="col-12"> |
|||
<?= GridView::widget([ |
|||
'dataProvider' => $dataProvider, |
|||
'filter' => $this->render("_search", ['model' => $searchModel]), |
|||
'batch' => [ |
|||
[ |
|||
"label" => "删除", |
|||
"url" => "comment/deletes" |
|||
], |
|||
], |
|||
'columns' => $columns, |
|||
'batchTemplate' => '', |
|||
'create' => '', |
|||
'export' => '', |
|||
]); |
|||
?>
|
|||
</div> |
|||
</div> |
@ -0,0 +1,19 @@ |
|||
<?php |
|||
|
|||
use yii\helpers\Html; |
|||
|
|||
/* @var $this yii\web\View */ |
|||
/* @var $model backend\modules\shop\models\ars\Comment */ |
|||
|
|||
$this->title = '编辑 Comment: ' . $model->id; |
|||
$this->params['breadcrumbs'][] = ['label' => 'Comments', 'url' => ['index']]; |
|||
$this->params['breadcrumbs'][] = ['label' => $model->id, 'url' => ['view', 'id' => $model->id]]; |
|||
$this->params['breadcrumbs'][] = 'Update '; |
|||
?>
|
|||
<div class="comment-update"> |
|||
|
|||
<?= $this->render('_form', [ |
|||
'model' => $model, |
|||
]) ?>
|
|||
|
|||
</div> |
@ -0,0 +1,47 @@ |
|||
<?php |
|||
|
|||
use backend\modules\shop\models\ars\Comment; |
|||
use yii\helpers\Html; |
|||
use yii\widgets\DetailView; |
|||
|
|||
/* @var $this yii\web\View */ |
|||
/* @var $model backend\modules\shop\models\ars\Comment */ |
|||
|
|||
$this->title = $model->id; |
|||
$this->params['breadcrumbs'][] = ['label' => 'Comments', 'url' => ['index']]; |
|||
$this->params['breadcrumbs'][] = $this->title; |
|||
\yii\web\YiiAsset::register($this); |
|||
?>
|
|||
<div class="comment-view"> |
|||
|
|||
<p> |
|||
<?= Html::a('返回列表', ['index'], ['class' => 'btn btn-success']) ?>
|
|||
</p> |
|||
|
|||
<?= DetailView::widget([ |
|||
'model' => $model, |
|||
'attributes' => [ |
|||
'id', |
|||
'user_id', |
|||
[ |
|||
'attribute' => 'order_goods_id', |
|||
'value' => function ($model) { |
|||
return $model->orderGoods->goods_name; |
|||
}, |
|||
'label' => '商品名称' |
|||
], |
|||
'star', |
|||
'content:ntext', |
|||
[ |
|||
'attribute' => 'status', |
|||
'value' => function ($model) { |
|||
return Comment::$commentStatus[$model->status]; |
|||
}, |
|||
'label' => '状态' |
|||
], |
|||
'updated_at:datetime', |
|||
'created_at:datetime', |
|||
], |
|||
]) ?>
|
|||
|
|||
</div> |
@ -1,27 +0,0 @@ |
|||
<?php |
|||
/* |
|||
* The MIT License |
|||
* |
|||
* Copyright 2019 Blobt. |
|||
* |
|||
* Permission is hereby granted, free of charge, to any person obtaining a copy |
|||
* of this software and associated documentation files (the "Software"), to deal |
|||
* in the Software without restriction, including without limitation the rights |
|||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
|||
* copies of the Software, and to permit persons to whom the Software is |
|||
* furnished to do so, subject to the following conditions: |
|||
* |
|||
* The above copyright notice and this permission notice shall be included in |
|||
* all copies or substantial portions of the Software. |
|||
* |
|||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
|||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
|||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
|||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
|||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
|||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN |
|||
* THE SOFTWARE. |
|||
*/ |
|||
?>
|
|||
|
|||
配置页 |
@ -0,0 +1,52 @@ |
|||
<?php |
|||
|
|||
use iron\widgets\Menu; |
|||
|
|||
?>
|
|||
<aside class="main-sidebar sidebar-dark-info elevation-4"> |
|||
<a href="#" class="brand-link"> |
|||
<img src="/img/logo.jpeg" alt="AdminLTE" class="brand-image img-circle elevation-3" |
|||
style="opacity: .8"> |
|||
<span class="brand-text font-weight-light">AdminLTE 3</span> |
|||
</a> |
|||
<!-- sidebar: style can be found in sidebar.less --> |
|||
<section class="sidebar"> |
|||
<?php |
|||
echo Menu::widget([ |
|||
'items' => [ |
|||
// ['label' => 'MAIN NAVIGATION', 'is_header' => true],
|
|||
['label' => '商城管理', 'url' => '#', 'icon' => 'far fa-store', 'items' => [ |
|||
['label' => '运营数据', 'url' => ['site/index', 'tag' => 'new']], |
|||
['label' => '基础配置', 'url' => ['config/index', 'tag' => 'new']], |
|||
] |
|||
], |
|||
['label' => '商品管理', 'url' => '#', 'icon' => 'far fa-box', 'items' => [ |
|||
['label' => '后台商品分类', 'url' => ['/goods/category/index']], |
|||
['label' => '规格管理', 'url' => ['/goods/attribute/index']], |
|||
['label' => '前端商品分类', 'url' => ['/goods/shop-category/index']], |
|||
['label' => '品牌管理', 'url' => ['/goods/brand/index']], |
|||
['label' => '供应商管理', 'url' => ['/goods/supplier/index']], |
|||
['label' => '商品列表', 'url' => ['/goods/goods/index']], |
|||
] |
|||
], |
|||
['label' => '订单管理', 'url' => '#', 'icon' => 'far fa-list-alt', 'items' => [ |
|||
['label' => '订单列表', 'url' => ['/shop/order/index', 'tag' => 'new']], |
|||
], |
|||
], |
|||
['label' => '配送服务', 'url' => '#', 'icon' => 'far fa-shipping-fast', 'items' => [ |
|||
['label' => '上门自提', 'url' => ['/shop/taking-site/index']], |
|||
['label' => '运费模板', 'url' => ['/shop/express-template/index']], |
|||
] |
|||
], |
|||
['label' => '售后管理', 'url' => '#', 'icon' => 'far fa-retweet', 'items' => [ |
|||
['label' => '售后管理', 'url' => ['/shop/after-sale/index']], |
|||
['label' => '评论', 'url' => ['/shop/comment/index']], |
|||
] |
|||
], |
|||
|
|||
] |
|||
]); |
|||
?>
|
|||
</section> |
|||
<!-- /.sidebar --> |
|||
</aside> |
@ -0,0 +1,17 @@ |
|||
{ |
|||
"presets": [ |
|||
"@babel/preset-react", |
|||
"@babel/preset-env" |
|||
], |
|||
"plugins": [ |
|||
["@babel/plugin-proposal-decorators", { "legacy": true }], |
|||
["@babel/proposal-class-properties", { "loose": true }], |
|||
"@babel/plugin-transform-runtime", |
|||
"babel-plugin-styled-components", |
|||
["import", { |
|||
"libraryName": "antd", |
|||
"libraryDirectory": "es", |
|||
"style": "css" |
|||
}] |
|||
] |
|||
} |
@ -0,0 +1,26 @@ |
|||
{ |
|||
"env": { |
|||
"browser": true, |
|||
"es6": true |
|||
}, |
|||
"extends": "eslint:recommended", |
|||
"globals": { |
|||
"Atomics": "readonly", |
|||
"SharedArrayBuffer": "readonly" |
|||
}, |
|||
"parserOptions": { |
|||
"ecmaFeatures": { |
|||
"jsx": true |
|||
}, |
|||
"ecmaVersion": 2018, |
|||
"sourceType": "module" |
|||
}, |
|||
"plugins": [ |
|||
"react", |
|||
"react-hooks" |
|||
], |
|||
"rules": { |
|||
"react-hooks/rules-of-hooks": "error", |
|||
"react-hooks/exhaustive-deps": "warn" |
|||
} |
|||
} |
@ -0,0 +1,11 @@ |
|||
/index.php |
|||
/index-test.php |
|||
/robots.txt |
|||
uploads |
|||
css/umeditor/php/upload/ |
|||
|
|||
node_modules/ |
|||
yarn-error.log |
|||
yarn.lock |
|||
package-lock.json |
|||
ueditor |
@ -0,0 +1,89 @@ |
|||
{ |
|||
"scripts": { |
|||
"dev": "webpack --mode=development --watch", |
|||
"dev:dashboard": "webpack --module dashboard --mode=development --watch", |
|||
"build:dashboard": "webpack --module dashboard --mode=production", |
|||
"dev:mini-program-management": "webpack --module mini_program_management --mode=development --watch", |
|||
"build:mini-program-management": "webpack --module mini_program_management --mode=production", |
|||
"dev:spread": "webpack --module spread --mode=development --watch", |
|||
"build:spread": "webpack --module spread --mode=production", |
|||
"dev:sku": "webpack --module sku --mode=development --watch", |
|||
"build:sku": "webpack --module sku --mode=production", |
|||
"dev:style": "webpack --module style --mode=development --watch", |
|||
"build:style": "webpack --module style --mode=production", |
|||
"dev:order-detail": "webpack --module order_detail --mode=development --watch", |
|||
"build:order-detail": "webpack --module order_detail --mode=production", |
|||
"dev:sku-item": "webpack --module sku_item --mode=development --watch", |
|||
"build:sku-item": "webpack --module sku_item --mode=production", |
|||
"dev:custom-menu": "webpack --module custom-menu --mode=development --watch", |
|||
"build:custom-menu": "webpack --module custom-menu --mode=production", |
|||
"dev:sku-for-activity": "webpack --module sku_for_activity --mode=development --watch", |
|||
"build:sku-for-activity": "webpack --module sku_for_activity --mode=production", |
|||
"build": "webpack --mode=production" |
|||
}, |
|||
"dependencies": { |
|||
"@babel/core": "^7.0.1", |
|||
"@babel/preset-react": "^7.0.0", |
|||
"antd": "^3.16.5", |
|||
"axios": "^0.18.0", |
|||
"babel-loader": "^8.0.2", |
|||
"bizcharts": "^3.4.3", |
|||
"che-react-number-easing": "^0.1.2", |
|||
"classnames": "^2.2.6", |
|||
"clone": "^2.1.2", |
|||
"compare-versions": "^3.4.0", |
|||
"dayjs": "^1.8.0", |
|||
"extract-text-webpack-plugin": "^4.0.0-beta.0", |
|||
"fast-deep-equal": "^2.0.1", |
|||
"file-loader": "^2.0.0", |
|||
"html2canvas": "^1.0.0-alpha.12", |
|||
"immer": "^2.1.5", |
|||
"mobx": "^5.6.0", |
|||
"mobx-react": "^5.3.6", |
|||
"polished": "^2.3.3", |
|||
"prop-types": "^15.6.2", |
|||
"qs": "^6.6.0", |
|||
"rasterizehtml": "^1.3.0", |
|||
"rc-switch": "^1.8.0", |
|||
"rc-upload": "^2.6.3", |
|||
"react": "^16.8.6", |
|||
"react-beautiful-dnd": "^11.0.2", |
|||
"react-chartjs-2": "^2.7.4", |
|||
"react-circle": "^1.1.1", |
|||
"react-color": "^2.17.0", |
|||
"react-dom": "^16.8.6", |
|||
"react-grid-layout": "^0.16.6", |
|||
"react-select": "^2.1.1", |
|||
"react-sortable-hoc": "^0.8.3", |
|||
"sass-resources-loader": "^1.3.3", |
|||
"styled-components": "^4.1.3", |
|||
"throttle-debounce": "^2.1.0", |
|||
"to-string-loader": "^1.1.5", |
|||
"url-loader": "^1.1.1", |
|||
"uuid": "^3.3.2", |
|||
"webpack": "^4.29.5", |
|||
"webpack-cli": "^3.2.3" |
|||
}, |
|||
"devDependencies": { |
|||
"@babel/plugin-proposal-class-properties": "^7.1.0", |
|||
"@babel/plugin-proposal-decorators": "^7.1.2", |
|||
"@babel/plugin-transform-runtime": "^7.4.3", |
|||
"@babel/preset-env": "^7.1.0", |
|||
"babel-plugin-import": "^1.11.0", |
|||
"babel-plugin-styled-components": "^1.10.0", |
|||
"clean-webpack-plugin": "^2.0.0", |
|||
"copy-webpack-plugin": "^4.6.0", |
|||
"css-loader": "^1.0.0", |
|||
"eslint": "^5.16.0", |
|||
"eslint-plugin-react": "^7.12.4", |
|||
"eslint-plugin-react-hooks": "^1.6.0", |
|||
"html-webpack-plugin": "^3.2.0", |
|||
"mini-css-extract-plugin": "^0.5.0", |
|||
"mobx-react-devtools": "^6.0.3", |
|||
"node-sass": "^4.9.3", |
|||
"require-context": "^1.1.0", |
|||
"resolve-url-loader": "^2.3.1", |
|||
"sass-loader": "^7.1.0", |
|||
"style-loader": "^0.23.0" |
|||
} |
|||
} |
@ -0,0 +1 @@ |
|||
<link rel="stylesheet" href="//at.alicdn.com/t/font_827976_jtnsfqxuiaf.css"> |
@ -0,0 +1,4 @@ |
|||
<div id="app"></div> |
|||
<script> |
|||
var csrfToken = "<?= Yii::$app->request->csrfToken ?>"; |
|||
</script> |
@ -0,0 +1,36 @@ |
|||
import React, {Component} from "react"; |
|||
import GridLayout from "react-grid-layout"; |
|||
import styled from 'styled-components' |
|||
import "react-grid-layout/css/styles.css"; |
|||
import "react-resizable/css/styles.css"; |
|||
import {inject, observer} from 'mobx-react' |
|||
import Module from './Module' |
|||
|
|||
@inject('store') |
|||
@observer |
|||
export default class GridEditor extends React.Component { |
|||
onLayoutChange = (layout) => { |
|||
const {changeLayout} = this.props.store; |
|||
changeLayout(layout); |
|||
}; |
|||
|
|||
render() { |
|||
const {layout, attrs, add} = this.props.store; |
|||
|
|||
return ( |
|||
<GridLayout |
|||
layout={layout} |
|||
cols={12} |
|||
rowHeight={30} |
|||
width={300} |
|||
onLayoutChange={this.onLayoutChange} |
|||
> |
|||
{layout.map(item => ( |
|||
<div key={item.i}> |
|||
<Module attr={attrs[item.i]} /> |
|||
</div> |
|||
))} |
|||
</GridLayout> |
|||
) |
|||
} |
|||
} |
@ -0,0 +1,101 @@ |
|||
import React from "react"; |
|||
import styled from "styled-components"; |
|||
import PropTypes from 'prop-types' |
|||
import Swiper from './modules/Swiper' |
|||
import GoodsList from './modules/GoodsList' |
|||
|
|||
export default class Module extends React.Component { |
|||
onLayoutChange = (layout) => { |
|||
const {changeLayout} = this.props.store; |
|||
changeLayout(layout); |
|||
}; |
|||
|
|||
static defaultProps = { |
|||
layout: {}, |
|||
attr: {} |
|||
}; |
|||
|
|||
static propTypes = { |
|||
layout: PropTypes.object, |
|||
attr: PropTypes.object |
|||
}; |
|||
|
|||
render() { |
|||
const {attr} = this.props; |
|||
|
|||
const moduleTable = { |
|||
Swiper: <Swiper />, |
|||
GoodsList: <GoodsList title={attr.title} type={attr.type} name={attr.data} /> |
|||
}; |
|||
|
|||
if (moduleTable[attr.component]) { |
|||
return ( |
|||
<Wrapper> |
|||
{moduleTable[attr.component]} |
|||
<HoverLayer> |
|||
<EditButton className='fa fa-edit' /> |
|||
</HoverLayer> |
|||
</Wrapper> |
|||
) |
|||
} else { |
|||
return ( |
|||
<ErrorTip>{attr.component}</ErrorTip> |
|||
) |
|||
} |
|||
} |
|||
} |
|||
|
|||
const Wrapper = styled.div`
|
|||
position: relative; |
|||
height: 100%; |
|||
background: #fff; |
|||
box-shadow: 0 0 40px 0 rgba(0, 0, 0, .1); |
|||
overflow: hidden; |
|||
`;
|
|||
|
|||
const ErrorTip = styled.div`
|
|||
display: flex; |
|||
word-break: break-all; |
|||
height: 100%; |
|||
justify-content: center; |
|||
align-items: center; |
|||
background: #efefef; |
|||
`;
|
|||
|
|||
const HoverLayer = styled.div`
|
|||
position: absolute; |
|||
top: 0; |
|||
left: 0; |
|||
width: 100%; |
|||
height: 100%; |
|||
display: flex; |
|||
justify-content: center; |
|||
align-items: center; |
|||
opacity: 0; |
|||
background: rgba(0, 0, 0, .6); |
|||
z-index: auto; |
|||
transition: all .3s; |
|||
&:hover { |
|||
opacity: 1; |
|||
z-index: 100; |
|||
} |
|||
`;
|
|||
|
|||
const buttonSize = 40; |
|||
const EditButton = styled.div`
|
|||
display: flex; |
|||
justify-content: center; |
|||
align-items: center; |
|||
width: ${buttonSize}px; |
|||
height: ${buttonSize}px; |
|||
border-radius: 100%; |
|||
background: #fff; |
|||
transition: all .1s; |
|||
font-size: ${buttonSize / 2}px; |
|||
font-weight: bold; |
|||
color: #000; |
|||
&:active { |
|||
background:#eee; |
|||
transform: scale(.8); |
|||
} |
|||
`;
|
@ -0,0 +1,106 @@ |
|||
import React, {Component} from "react"; |
|||
import GridEditor from './GridEditor' |
|||
import styled from 'styled-components' |
|||
|
|||
export default class Previewer extends React.Component { |
|||
state = { |
|||
time: `${new Date().getHours()}:${new Date().getMinutes()}` |
|||
}; |
|||
|
|||
componentWillMount() { |
|||
this.timer = setInterval(() => { |
|||
const now = new Date(); |
|||
this.setState({ |
|||
time: `${now.getHours()}:${now.getMinutes().toString().padStart(2, '0')}` |
|||
}) |
|||
}, 1000) |
|||
} |
|||
|
|||
componentWillUnmount() { |
|||
this.timer && clearInterval(this.timer); |
|||
} |
|||
|
|||
render() { |
|||
const {time} = this.state; |
|||
// layout is an array of objects, see the demo for more complete usage
|
|||
|
|||
return ( |
|||
<Wrapper> |
|||
<Header> |
|||
<StatusBar> |
|||
<span>{time}</span> |
|||
</StatusBar> |
|||
<TitleBar> |
|||
<span>首页</span> |
|||
</TitleBar> |
|||
</Header> |
|||
<Content> |
|||
<GridEditor /> |
|||
</Content> |
|||
<Tabbar /> |
|||
</Wrapper> |
|||
) |
|||
} |
|||
} |
|||
|
|||
const ratio = 16 / 9; |
|||
const width = 300; |
|||
|
|||
const Wrapper = styled.div`
|
|||
display: flex; |
|||
flex-direction: column; |
|||
width: ${width}px; |
|||
height: ${width * ratio}px; |
|||
border: 1px solid #eee; |
|||
border-radius: 10px; |
|||
overflow-x: hidden; |
|||
background: #fff; |
|||
box-shadow: 0 0 40px 0 rgba(0, 0, 0, .1); |
|||
`;
|
|||
|
|||
const Header = styled.div`
|
|||
flex-shrink: 0; |
|||
width: 100%; |
|||
background-size: cover; |
|||
background-repeat: no-repeat; |
|||
text-align: center; |
|||
font-size: 10px; |
|||
margin-bottom: 3px; |
|||
`;
|
|||
|
|||
const SimulationBar = styled.div`
|
|||
width: 100%; |
|||
background-size: contain; |
|||
background-repeat: no-repeat; |
|||
`;
|
|||
|
|||
const StatusBar = styled(SimulationBar)`
|
|||
background-image: url("/img/status-bar.png"); |
|||
background-size: cover; |
|||
`;
|
|||
|
|||
const TitleBar = styled(SimulationBar)`
|
|||
display: flex; |
|||
align-items: center; |
|||
height: 38px; |
|||
background-image: url("/img/weapp-status-bar.png"); |
|||
span { |
|||
margin-left: 10px; |
|||
font-size: 1.4em; |
|||
} |
|||
`;
|
|||
|
|||
const Tabbar = styled(SimulationBar)`
|
|||
flex-shrink: 0; |
|||
width: 100%; |
|||
height: 40px; |
|||
border-top: 1px solid #eee; |
|||
background-size: contain; |
|||
background-repeat: no-repeat; |
|||
background-image: url('/img/tabbar.png'); |
|||
`;
|
|||
|
|||
const Content = styled.div`
|
|||
flex-grow: 1; |
|||
overflow-y: auto; |
|||
`;
|
@ -0,0 +1,16 @@ |
|||
import React, {Component} from "react"; |
|||
import styled from 'styled-components' |
|||
|
|||
export default class PropsEditor extends React.Component { |
|||
render() { |
|||
return ( |
|||
<Wrapper> |
|||
|
|||
</Wrapper> |
|||
) |
|||
} |
|||
} |
|||
|
|||
const Wrapper = styled.div`
|
|||
flex-grow: 1; |
|||
`;
|
@ -0,0 +1,29 @@ |
|||
import React, {Component} from 'react'; |
|||
import {render} from "react-dom"; |
|||
import axios from 'axios' |
|||
import styled from 'styled-components' |
|||
import Previewer from './Previewer' |
|||
import PropsEditor from './PropsEditor' |
|||
import {Provider} from 'mobx-react' |
|||
import store from './store' |
|||
import DevTools from 'mobx-react-devtools'; |
|||
|
|||
const Wrapper = styled.div`
|
|||
display: flex; |
|||
`;
|
|||
|
|||
class AmazingCreator extends React.Component { |
|||
render() { |
|||
return ( |
|||
<Provider store={store}> |
|||
<Wrapper> |
|||
<Previewer /> |
|||
<PropsEditor /> |
|||
<DevTools /> |
|||
</Wrapper> |
|||
</Provider> |
|||
) |
|||
} |
|||
} |
|||
|
|||
render(<AmazingCreator />, document.getElementById('edit-home')); |
@ -0,0 +1,67 @@ |
|||
import React from "react"; |
|||
import PropTypes from "prop-types"; |
|||
import styled from 'styled-components' |
|||
|
|||
export default class GoodsList extends React.Component { |
|||
static propTypes = { |
|||
name: PropTypes.string, |
|||
title: PropTypes.string, |
|||
type: PropTypes.string, |
|||
onChangeTitle: PropTypes.func, |
|||
onChangeType: PropTypes.func |
|||
}; |
|||
|
|||
render() { |
|||
const {name, title, type, onChangeTitle, onChangeType} = this.props; |
|||
const typeImageTable = { |
|||
grid: '/img/grid.png', |
|||
list: '/img/list.png', |
|||
horizScroll: '/img/scroll.png', |
|||
}; |
|||
|
|||
const options = [ |
|||
{value: 1, label: '横排(右滚动)'}, |
|||
{value: 2, label: '多列(三列)'}, |
|||
{value: 3, label: '竖排(下滚动)'} |
|||
]; |
|||
|
|||
return ( |
|||
<Wrapper> |
|||
<Header> |
|||
<Title>{title}</Title> |
|||
<More>查看更多></More> |
|||
</Header> |
|||
<Image src={typeImageTable[type]} draggable="false" /> |
|||
</Wrapper> |
|||
) |
|||
} |
|||
} |
|||
|
|||
const Wrapper = styled.div`
|
|||
width: 100%; |
|||
`;
|
|||
|
|||
const Header = styled.div`
|
|||
display: flex; |
|||
justify-content: space-between; |
|||
align-items: center; |
|||
padding: 5px 10px; |
|||
`;
|
|||
|
|||
const Title = styled.div`
|
|||
color: #e2314a; |
|||
font-weight: bold; |
|||
`;
|
|||
|
|||
const More = styled.div`
|
|||
font-size: .8em; |
|||
color: #aaa; |
|||
cursor: pointer; |
|||
&:active { |
|||
color: #666; |
|||
} |
|||
`;
|
|||
|
|||
const Image = styled.img`
|
|||
width: 100%; |
|||
`;
|
@ -0,0 +1,14 @@ |
|||
import React from "react"; |
|||
import styled from 'styled-components' |
|||
|
|||
export default class Swiper extends React.Component { |
|||
render() { |
|||
return <Image src='/img/banner.png' draggable="false" /> |
|||
} |
|||
} |
|||
|
|||
const Image = styled.img`
|
|||
width: 100%; |
|||
height: 100%; |
|||
user-select: none; |
|||
`;
|
@ -0,0 +1,150 @@ |
|||
import React, {Component} from 'react'; |
|||
import { |
|||
SortableContainer, |
|||
SortableElement, |
|||
SortableHandle, |
|||
arrayMove, |
|||
} from 'react-sortable-hoc'; |
|||
import PropTypes from 'prop-types' |
|||
import Select from 'react-select'; |
|||
import axios from 'axios' |
|||
|
|||
const DragHandle = SortableHandle(() => ( |
|||
<div className='drag-handle glyphicon glyphicon-menu-hamburger' /> |
|||
)); // This can be any component you want
|
|||
|
|||
class Part extends React.Component { |
|||
static propTypes = { |
|||
name: PropTypes.string, |
|||
title: PropTypes.string, |
|||
onChangeTitle: PropTypes.func, |
|||
onChangeType: PropTypes.func |
|||
}; |
|||
|
|||
render() { |
|||
const {name, title, type, onChangeTitle, onChangeType} = this.props; |
|||
const typeImageTable = [ |
|||
'/img/scroll.png', |
|||
'/img/grid.png', |
|||
'/img/list.png', |
|||
]; |
|||
const options = [ |
|||
{ value: 1, label: '横排(右滚动)' }, |
|||
{ value: 2, label: '多列(三列)' }, |
|||
{ value: 3, label: '竖排(下滚动)' } |
|||
]; |
|||
|
|||
return ( |
|||
<div className="part"> |
|||
<div className="part__handle"> |
|||
<input type="text" value={title} onChange={onChangeTitle} /> |
|||
<Select |
|||
value={options[type - 1]} |
|||
onChange={onChangeType} |
|||
options={options} |
|||
isSearchable={false} |
|||
/> |
|||
</div> |
|||
<div className="part__content"> |
|||
<div className="part__header"> |
|||
<div className="title">{title}</div> |
|||
<div className="more">查看更多></div> |
|||
</div> |
|||
<img src={typeImageTable[type - 1]} alt=""/> |
|||
</div> |
|||
</div> |
|||
) |
|||
} |
|||
} |
|||
|
|||
const SortableItem = SortableElement(({value}) => { |
|||
return ( |
|||
<li className='sortable-item'> |
|||
<DragHandle /> |
|||
{value} |
|||
</li> |
|||
); |
|||
}); |
|||
|
|||
const SortableList = SortableContainer(({items}) => { |
|||
return ( |
|||
<ul> |
|||
{items.map((value, index) => ( |
|||
<SortableItem key={`item-${index}`} index={index} value={value} /> |
|||
))} |
|||
</ul> |
|||
); |
|||
}); |
|||
|
|||
class SortableComponent extends Component { |
|||
state = { |
|||
items: data, |
|||
}; |
|||
|
|||
onSortEnd = ({oldIndex, newIndex}) => { |
|||
const {items} = this.state; |
|||
|
|||
this.setState({ |
|||
items: arrayMove(items, oldIndex, newIndex), |
|||
}); |
|||
}; |
|||
|
|||
onChangeTitle = (index, event) => { |
|||
let {items} = this.state; |
|||
items[index].title = event.target.value; |
|||
this.setState({ |
|||
items |
|||
}) |
|||
}; |
|||
|
|||
onChangeType = (index, newType) => { |
|||
let {items} = this.state; |
|||
items[index].type = newType.value; |
|||
this.setState({ |
|||
items |
|||
}) |
|||
}; |
|||
|
|||
submit = () => { |
|||
const {items} = this.state; |
|||
axios.post(location.href, { |
|||
'_csrf-api': csrf, |
|||
data: items |
|||
}).then((res) => { |
|||
if (res.data === 1) { |
|||
this.back(); |
|||
} |
|||
}) |
|||
}; |
|||
|
|||
back = () => { |
|||
history.go(-1) |
|||
}; |
|||
|
|||
render() { |
|||
const {items} = this.state; |
|||
|
|||
const itemsView = items.map((item, index) => ( |
|||
<Part |
|||
key={index} |
|||
title={item.title} |
|||
type={item.type} |
|||
onChangeTitle={this.onChangeTitle.bind(undefined, index)} |
|||
onChangeType={this.onChangeType.bind(undefined, index)} |
|||
/> |
|||
)); |
|||
|
|||
return ( |
|||
<div> |
|||
<SortableList |
|||
items={itemsView} |
|||
lockAxis='y' |
|||
onSortEnd={this.onSortEnd} |
|||
useDragHandle={true} |
|||
/> |
|||
<button type='button' className='btn btn-success' onClick={this.submit}>保存</button> |
|||
<button className='btn btn-info' onClick={this.back}>返回</button> |
|||
</div> |
|||
); |
|||
} |
|||
} |
@ -0,0 +1,68 @@ |
|||
import {observable, action, configure} from "mobx"; |
|||
|
|||
configure({ |
|||
enforceActions: 'always' |
|||
}); |
|||
|
|||
class Store { |
|||
@observable currentId = 3; |
|||
|
|||
@observable layout = [ |
|||
{ |
|||
i: '1', |
|||
x: 0, |
|||
y: 0, |
|||
w: 12, |
|||
h: 3 |
|||
}, |
|||
{ |
|||
i: '2', |
|||
x: 1, |
|||
y: 2, |
|||
w: 12, |
|||
h: 3 |
|||
}, |
|||
{ |
|||
i: '3', |
|||
x: 2, |
|||
y: 3, |
|||
w: 12, |
|||
h: 3 |
|||
}, |
|||
]; |
|||
|
|||
@observable attrs = { |
|||
1: { |
|||
component: 'Swiper', |
|||
}, |
|||
2: { |
|||
component: 'GoodsList', |
|||
type: 'grid', |
|||
title: '新品推荐', |
|||
data: 'best' |
|||
}, |
|||
3: { |
|||
component: 'GoodsList', |
|||
type: 'list', |
|||
title: '火爆热销', |
|||
data: 'hot' |
|||
} |
|||
}; |
|||
|
|||
@action.bound changeLayout(layout) { |
|||
this.layout = layout; |
|||
} |
|||
|
|||
@action.bound add(attr) { |
|||
this.layout.push({ |
|||
i: (++this.currentId).toString(), |
|||
x: 2, |
|||
y: 3, |
|||
w: 12, |
|||
h: 3 |
|||
}); |
|||
this.attrs[this.currentId] = attr; |
|||
} |
|||
} |
|||
|
|||
export default new Store() |
@ -0,0 +1,313 @@ |
|||
import React, {useState, useEffect} from 'react' |
|||
import ReactDOM from 'react-dom' |
|||
import {Table, Select, Input, InputNumber, Button, message, Modal, Spin, LocaleProvider} from "antd"; |
|||
import styled from 'styled-components' |
|||
import produce from 'immer' |
|||
import axios from 'axios' |
|||
import zhCN from 'antd/lib/locale-provider/zh_CN'; |
|||
import queryString from 'qs'; |
|||
import deepEqual from 'fast-deep-equal' |
|||
import deepClone from 'clone' |
|||
import '../utils/ajax' |
|||
|
|||
const {sku: defaultSku, attributes: defaultAttributes} = window; |
|||
|
|||
const Option = Select.Option; |
|||
|
|||
const Fragment = React.Fragment; |
|||
|
|||
const additionalCol = [ |
|||
{ |
|||
key: 'price', |
|||
title: '价格', |
|||
precision: 2, |
|||
min: 0 |
|||
}, |
|||
{ |
|||
key: 'stock', |
|||
title: '库存(-1 为不限库存)', |
|||
precision: 0, |
|||
min: -1 |
|||
}, |
|||
{ |
|||
key: 'weight', |
|||
title: '重量(kg)', |
|||
precision: 2, |
|||
min: 0 |
|||
} |
|||
]; |
|||
|
|||
function buildInputCol(data, onChange) { |
|||
return data.map(({key, title, ...others}) => ({ |
|||
title, |
|||
dataIndex: key, |
|||
key, |
|||
align: 'center', |
|||
render: (text, _, index) => ( |
|||
<InputNumber |
|||
type='number' |
|||
value={text} |
|||
{...others} |
|||
onChange={value => onChange(index, key, value)} |
|||
/> |
|||
) |
|||
})) |
|||
} |
|||
|
|||
function SkuTable({ skuData, attributes, type, onChangeItem, onDeleteItem }) { |
|||
function setSkuItem(skuIndex, skuItemIndex, skuItemValue) { |
|||
const newValue = Object.assign(skuData[skuIndex].value, { |
|||
[skuItemIndex]: skuItemValue |
|||
}); |
|||
|
|||
onChangeItem(skuIndex, 'value', newValue) |
|||
} |
|||
|
|||
const dataSource = skuData.map(({ id, value, price, stock, weight }, index) => { |
|||
const attrList = type === 'select' ? value.reduce((accumulator, currentValue, currentIndex) => ({ |
|||
...accumulator, |
|||
[currentIndex]: currentValue |
|||
}), {}) : { value }; |
|||
|
|||
return { |
|||
key: index, |
|||
...attrList, |
|||
price, |
|||
stock, |
|||
weight |
|||
} |
|||
}); |
|||
|
|||
const skuColumns = type === 'select' ? attributes.map(({id, name, attrValue}, index) => ({ |
|||
title: name, |
|||
dataIndex: index, |
|||
key: id, |
|||
align: 'center', |
|||
render: (value, _, rowIndex) => ( |
|||
<Select |
|||
value={value} |
|||
style={{ width: '100%', minWidth: '50px' }} |
|||
onChange={newValue => setSkuItem(rowIndex, index, newValue)} |
|||
> |
|||
{attrValue.map(({ id, attr_value }) => ( |
|||
<Option key={id} value={id}>{attr_value}</Option> |
|||
))} |
|||
</Select> |
|||
) |
|||
})) : [{ |
|||
title: '规格', |
|||
dataIndex: 'value', |
|||
key: 'value', |
|||
align: 'center', |
|||
render: (value, _, rowIndex) => ( |
|||
<Input |
|||
value={value} |
|||
style={{ width: '100%', minWidth: '50px' }} |
|||
onChange={e => onChangeItem(rowIndex, 'value', e.target.value)} |
|||
/> |
|||
) |
|||
}]; |
|||
|
|||
const columns = [ |
|||
...skuColumns, |
|||
...buildInputCol(additionalCol, onChangeItem), |
|||
{ |
|||
key: 'delete', |
|||
title: '操作', |
|||
render: (_1, _2, index) => <a href="javascript:void(0);" onClick={() => onDeleteItem(index)}>删除</a> |
|||
} |
|||
]; |
|||
|
|||
return ( |
|||
<Fragment> |
|||
<Table dataSource={dataSource} columns={columns} pagination={false} /> |
|||
</Fragment> |
|||
) |
|||
} |
|||
|
|||
function App() { |
|||
const goodsId = queryString.parse(location.search.substr(1)).id; |
|||
const defaultData = (Number(defaultSku.type) === 1 && defaultAttributes.length === 0) ? { |
|||
type: 2, |
|||
data: [] |
|||
} : defaultSku; |
|||
const [data, setData] = useState(defaultData); |
|||
const [initData, setInitData] = useState(deepClone(defaultData)); |
|||
const [attributes, setAttributes] = useState(defaultAttributes); |
|||
const [loading, setLoading] = useState(false); |
|||
const modeList = [ |
|||
{ |
|||
key: 'select', |
|||
title: '已选商品规格设置' |
|||
}, |
|||
{ |
|||
key: 'manual', |
|||
title: '手动输入' |
|||
}, |
|||
]; |
|||
|
|||
function setItem(skuIndex, key, value) { |
|||
setData(produce(data, draftState => { |
|||
draftState.data[skuIndex] = { |
|||
...draftState.data[skuIndex], |
|||
[key]: value |
|||
}; |
|||
})); |
|||
} |
|||
|
|||
function deleteItem(index) { |
|||
setData(produce(data, draftState => { |
|||
draftState.data.splice(index, 1) |
|||
})); |
|||
} |
|||
|
|||
const type = modeList[data.type - 1].key; |
|||
|
|||
function selectMode(mode) { |
|||
async function switchMode() { |
|||
const modeTable = { |
|||
select: 1, |
|||
manual: 2 |
|||
}; |
|||
setLoading(true); |
|||
const {sku, attributes} = await axios.get('switch', { |
|||
params: {goodsId, type: modeTable[mode]} |
|||
}); |
|||
setData(sku); |
|||
setInitData(deepClone(sku)); |
|||
setAttributes(attributes); |
|||
setLoading(false); |
|||
} |
|||
|
|||
if (mode === type) { |
|||
return null |
|||
} else if (mode === 'select' && attributes.length === 0) { |
|||
Modal.info({ content: '无已选规格,请到【规格管理】中添加规格并在【商品列表】>【修改】>【商品规格】中选择需要的规格' }) |
|||
} else if (!deepEqual(initData, data)) { |
|||
Modal.confirm({ |
|||
content: '修改的内容没有保存,是否放弃修改并切换?', |
|||
onOk: () => { switchMode() } |
|||
}); |
|||
} else { |
|||
return switchMode(); |
|||
} |
|||
} |
|||
|
|||
function addItem() { |
|||
const attrValue = type === 'select' ? { value: [] } : { value: '' }; |
|||
|
|||
setData({ |
|||
...data, |
|||
data: [ |
|||
...data.data, |
|||
{ |
|||
...attrValue, |
|||
id: -1, |
|||
price: null, |
|||
stock: -1, |
|||
weight: 0 |
|||
} |
|||
] |
|||
}) |
|||
} |
|||
|
|||
async function save() { |
|||
let errorMessage = '请填写完整的信息'; |
|||
|
|||
const isAllDataCorrect = data.data.every(item => { |
|||
const isAllNotEmpty = Object.keys(item) |
|||
.every(key => item[key] !== undefined && item[key] !== null && item[key] !== ''); |
|||
|
|||
if (type === 'select') { |
|||
const isAllSkuCorrect = item.value && item.value.length === attributes.length; |
|||
|
|||
if (!isAllSkuCorrect) errorMessage = '请选择所有的规格项'; |
|||
|
|||
return isAllNotEmpty && isAllSkuCorrect; |
|||
} else { |
|||
const isAllSkuCorrect = item.value && item.value.length > 0; |
|||
|
|||
if (!isAllSkuCorrect) errorMessage = '请填写所有的规格项'; |
|||
|
|||
return isAllNotEmpty && isAllSkuCorrect; |
|||
} |
|||
}); |
|||
|
|||
if (isAllDataCorrect) { |
|||
await axios.post('add-sku', { goodsId, sku: data}); |
|||
setInitData(deepClone(data)); |
|||
message.success('保存成功'); |
|||
history.back(); |
|||
} else { |
|||
message.error(errorMessage) |
|||
} |
|||
} |
|||
|
|||
return ( |
|||
<Wrapper> |
|||
<Spin spinning={loading} tip='加载中...'> |
|||
<SelectMode> |
|||
{modeList.map(({ key, title }) => ( |
|||
<ModeItem |
|||
key={key} |
|||
className={type === key ? 'active' : ''} |
|||
onClick={() => selectMode(key)} |
|||
>{title}</ModeItem> |
|||
))} |
|||
</SelectMode> |
|||
<SkuTable |
|||
skuData={data.data} |
|||
attributes={attributes} |
|||
type={type} |
|||
onChangeItem={setItem} |
|||
onDeleteItem={deleteItem} |
|||
/> |
|||
<ButtonGroup> |
|||
<Button type='primary' onClick={addItem}>新增一行</Button> |
|||
<Button type='primary' style={{ marginLeft: '20px' }} onClick={save}>保存</Button> |
|||
<Button style={{ marginLeft: '20px' }} onClick={() => history.back()}>返回</Button> |
|||
</ButtonGroup> |
|||
</Spin> |
|||
</Wrapper> |
|||
) |
|||
} |
|||
|
|||
const ButtonGroup = styled.div`
|
|||
margin-top: 20px; |
|||
`;
|
|||
|
|||
const Wrapper = styled.div`
|
|||
|
|||
`;
|
|||
|
|||
const SelectMode = styled.div`
|
|||
display: flex; |
|||
justify-content: center; |
|||
margin-bottom: 20px; |
|||
`;
|
|||
|
|||
const ModeItem = styled.div`
|
|||
padding: 5px 15px; |
|||
margin: 10px; |
|||
cursor: pointer; |
|||
border-radius: 30px; |
|||
color: #999; |
|||
background: #eee; |
|||
border: 2px solid transparent; |
|||
transition: all .3s; |
|||
&.active { |
|||
background: #1c7cff; |
|||
color: #fff; |
|||
cursor: default; |
|||
} |
|||
&:hover { |
|||
border-color: #1c7cff; |
|||
} |
|||
`;
|
|||
|
|||
ReactDOM.render( |
|||
<LocaleProvider locale={zhCN}> |
|||
<App /> |
|||
</LocaleProvider>, |
|||
document.getElementById('app') |
|||
); |
@ -0,0 +1,74 @@ |
|||
import React from "react"; |
|||
import styled from "styled-components"; |
|||
import { Select, Spin, Icon } from 'antd'; |
|||
import PropTypes from 'prop-types' |
|||
|
|||
const Option = Select.Option; |
|||
|
|||
const antIcon = <Icon type="loading" style={{ fontSize: 24 }} spin />; |
|||
|
|||
export default class SelectCell extends React.Component { |
|||
constructor(props) { |
|||
super(props); |
|||
this.state = { |
|||
skuList: null |
|||
}; |
|||
} |
|||
|
|||
static defaultProps = { |
|||
onChange: () => {} |
|||
}; |
|||
|
|||
static propTypes = { |
|||
items: PropTypes.array, |
|||
defaultValue: PropTypes.array, |
|||
value: PropTypes.array, |
|||
label: PropTypes.string, |
|||
onChange: PropTypes.func |
|||
}; |
|||
|
|||
render() { |
|||
const {items, label, value, defaultValue, onChange} = this.props; |
|||
|
|||
return ( |
|||
<Root> |
|||
<Label>{label}</Label> |
|||
{items ? ( |
|||
<Select |
|||
mode="multiple" |
|||
style={{width: '100%'}} |
|||
placeholder="请选择要使用的规格" |
|||
defaultValue={defaultValue} |
|||
value={value} |
|||
onChange={onChange} |
|||
> |
|||
{items.map(item => ( |
|||
<Option |
|||
key={item.id} |
|||
disabled={item.disabled} |
|||
title={item.name} |
|||
value={item.id} |
|||
>{item.name}</Option> |
|||
))} |
|||
</Select> |
|||
) : <Spin indicator={antIcon} />} |
|||
</Root> |
|||
) |
|||
} |
|||
} |
|||
|
|||
const Root = styled.div`
|
|||
display: flex; |
|||
align-items: center; |
|||
width: 100%; |
|||
margin: 10px 0; |
|||
input.ant-select-search__field:not([type="submit"]):not([type="button"]):not([type="reset"]) { |
|||
padding: 1px; |
|||
background: none!important; |
|||
} |
|||
`;
|
|||
|
|||
const Label = styled.div`
|
|||
width: 100px; |
|||
margin-right: 20px; |
|||
`;
|
@ -0,0 +1,157 @@ |
|||
import React from "react"; |
|||
import styled from "styled-components"; |
|||
import ReactDOM from "react-dom"; |
|||
import { LocaleProvider, Alert } from 'antd'; |
|||
import zhCN from 'antd/lib/locale-provider/zh_CN'; |
|||
import SelectCell from './SelectCell' |
|||
|
|||
const goodsId = window.goodsId; |
|||
const currentAttr = window.currentAttr.map(({id, name, value}) => ({id, name, value})); |
|||
const canNotDeleteAttr = window.canNotDeleteAttr; |
|||
|
|||
class Sku extends React.Component { |
|||
constructor(props) { |
|||
super(props); |
|||
|
|||
this.state = { |
|||
skuList: currentAttr, |
|||
allAttr: null, |
|||
}; |
|||
} |
|||
|
|||
componentDidMount() { |
|||
const select = $('#goods-cat_id'); |
|||
const loadData = (id, shouldClearSelectedAttrs = false) => { |
|||
$.ajax({ |
|||
cache: false, |
|||
url: "filter-attribute", |
|||
data: {catId: id, goodsId: goodsId}, |
|||
dataType: "json", |
|||
success: data => { |
|||
const allAttr = data.map(({id, name, value}, index) => { |
|||
const canNotDeleteCurrent = canNotDeleteAttr.find(i => i.id === parseInt(id)); |
|||
|
|||
return { |
|||
id: parseInt(id), |
|||
name, |
|||
disabled: Boolean(canNotDeleteCurrent), |
|||
value: value.split(',').map(i => ({ |
|||
id: i, |
|||
name: i, |
|||
disabled: Boolean(canNotDeleteCurrent) && canNotDeleteCurrent.value.some(j => j === i) |
|||
})) |
|||
} |
|||
}); |
|||
if (shouldClearSelectedAttrs) { |
|||
this.setState({ |
|||
skuList: [], |
|||
allAttr |
|||
}) |
|||
} else { |
|||
this.setState({ |
|||
allAttr |
|||
}) |
|||
} |
|||
} |
|||
}); |
|||
} |
|||
|
|||
loadData(select.val()) |
|||
select.change(function () { |
|||
loadData($(this).val(), true) |
|||
}) |
|||
} |
|||
|
|||
handleChangeAttr = (value) => { |
|||
let {skuList} = this.state; |
|||
|
|||
this.setState({ |
|||
skuList: value.map(id => { |
|||
let result = skuList.find(i => i.id === id); |
|||
|
|||
if (!result) { |
|||
const attr = this.state.allAttr.find(i => i.id === id); |
|||
if (attr) { |
|||
const {id, name, value} = attr; |
|||
result = {id, name, value: value.map(i => i.id)}; |
|||
} |
|||
} |
|||
|
|||
return result; |
|||
}) |
|||
}) |
|||
}; |
|||
|
|||
handleChangeAttrItem = (index, value) => { |
|||
let {skuList} = this.state; |
|||
skuList[index].value = value; |
|||
this.setState({ |
|||
skuList |
|||
}) |
|||
}; |
|||
|
|||
render() { |
|||
const {skuList, allAttr} = this.state; |
|||
if (allAttr) { |
|||
return ( |
|||
<Root> |
|||
{(canNotDeleteAttr && canNotDeleteAttr.length > 0) && ( |
|||
<Header> |
|||
<Alert |
|||
message="提示" |
|||
description="一些规格已经被使用,不能删除。若想删除请先到商品列表找到该商品, |
|||
点击右侧的“添加 SKU”按钮进入“SKU 管理页面”,删除使用此规格的所有 SKU,再回到这里进行删除。" |
|||
type="info" |
|||
showIcon |
|||
/> |
|||
</Header> |
|||
)} |
|||
<SelectGroup> |
|||
<SelectCell |
|||
label='规格类型' |
|||
items={allAttr} |
|||
value={skuList.map(i => i.id)} |
|||
onChange={this.handleChangeAttr} |
|||
/> |
|||
{skuList && skuList.map((sku, index) => ( |
|||
<SelectCell |
|||
key={sku.id} |
|||
label={sku.name} |
|||
items={allAttr.find(i => i.id === sku.id).value} |
|||
value={sku.value} |
|||
onChange={this.handleChangeAttrItem.bind(undefined, index)} |
|||
/> |
|||
))} |
|||
<input |
|||
type="hidden" |
|||
id="attribute" |
|||
name="attribute" |
|||
value={JSON.stringify(skuList.map(({id, value}) => ({id, value})))} |
|||
/> |
|||
</SelectGroup> |
|||
</Root> |
|||
) |
|||
} else { |
|||
return <></> |
|||
} |
|||
} |
|||
} |
|||
|
|||
const Root = styled.div`
|
|||
|
|||
`;
|
|||
|
|||
const Header = styled.div`
|
|||
margin-bottom: 30px; |
|||
`;
|
|||
|
|||
const SelectGroup = styled.div`
|
|||
width: 100%; |
|||
`;
|
|||
|
|||
ReactDOM.render( |
|||
<LocaleProvider locale={zhCN}> |
|||
<Sku /> |
|||
</LocaleProvider>, |
|||
document.getElementById('sku-choose') |
|||
); |
@ -0,0 +1,83 @@ |
|||
.react-resizable-handle { |
|||
z-index: 200; |
|||
} |
|||
|
|||
#edit-home { |
|||
width: 100%; |
|||
margin: auto; |
|||
//background: #eee; |
|||
ul { |
|||
padding: 0; |
|||
margin-bottom: 20px; |
|||
} |
|||
.btn { |
|||
margin-right: 10px; |
|||
} |
|||
} |
|||
|
|||
.amazing-creator, .ac { |
|||
display: flex; |
|||
&__board { |
|||
flex-grow: 1; |
|||
} |
|||
} |
|||
|
|||
.sortable-item { |
|||
display: flex; |
|||
position: relative; |
|||
list-style: none; |
|||
background: #fff; |
|||
margin: 10px 0; |
|||
padding: 10px; |
|||
border-radius: 10px; |
|||
box-shadow: 0 0 30px 0 rgba(0, 0, 0, .1); |
|||
.drag-handle { |
|||
display: flex; |
|||
align-items: center; |
|||
padding: 10px; |
|||
user-select: none; |
|||
cursor: row-resize; |
|||
color: #362c57; |
|||
font-size: 1.5em; |
|||
} |
|||
.part { |
|||
flex-grow: 1; |
|||
flex-shrink: 1; |
|||
display: flex; |
|||
justify-content: space-between; |
|||
&__handle { |
|||
display: flex; |
|||
flex-direction: column; |
|||
justify-content: center; |
|||
margin-left: 10px; |
|||
> * { |
|||
margin: 10px 0; |
|||
} |
|||
} |
|||
&__content { |
|||
flex-shrink: 0; |
|||
width: 250px; |
|||
img { |
|||
width: 100%; |
|||
} |
|||
} |
|||
&__header { |
|||
display: flex; |
|||
justify-content: space-between; |
|||
align-items: center; |
|||
.title { |
|||
color: #e2314a; |
|||
font-weight: bold; |
|||
margin-left: 15px; |
|||
} |
|||
.more { |
|||
font-size: .8em; |
|||
color: #aaa; |
|||
cursor: pointer; |
|||
&:active { |
|||
color: #666; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,90 @@ |
|||
.content-header { |
|||
display: flex; |
|||
flex-wrap: wrap; |
|||
justify-content: space-between; |
|||
align-items: center; |
|||
padding: 0; |
|||
h1, .breadcrumb { |
|||
margin: 5px 0!important; |
|||
} |
|||
h1 { |
|||
color: $color-primary; |
|||
font-weight: bold; |
|||
padding-left: 10px; |
|||
border-left: 5px solid $color-primary; |
|||
} |
|||
} |
|||
|
|||
.content-header>.breadcrumb { |
|||
$height: 25px; |
|||
$size: 10px; |
|||
display: flex; |
|||
flex-wrap: wrap; |
|||
position: static; |
|||
float: none!important; |
|||
height: $height; |
|||
background: none; |
|||
overflow: hidden; |
|||
padding: 0; |
|||
li { |
|||
display: flex; |
|||
align-items: center; |
|||
position: relative; |
|||
margin-right: $size + 3px; |
|||
background: $color-primary; |
|||
&:hover { |
|||
$background: darken($color-primary, 10%); |
|||
background: $background; |
|||
&::before { |
|||
border-top: $height / 2 solid $background; |
|||
border-bottom: $height / 2 solid $background; |
|||
} |
|||
&::after { |
|||
border-left: $size solid $background; |
|||
} |
|||
} |
|||
&::before { |
|||
content: ''; |
|||
position: absolute; |
|||
top: 0; |
|||
left: -1 * $size; |
|||
width: 0; |
|||
height: 0; |
|||
border-top: $height / 2 solid $color-primary; |
|||
border-bottom: $height / 2 solid $color-primary; |
|||
border-left: $size solid transparent; |
|||
} |
|||
&::after { |
|||
content: ''; |
|||
position: absolute; |
|||
top: 0; |
|||
right: -1 * $size; |
|||
width: 0; |
|||
height: 0; |
|||
border-top: $height / 2 solid transparent; |
|||
border-bottom: $height / 2 solid transparent; |
|||
border-left: $size solid $color-primary; |
|||
} |
|||
&.active { |
|||
$background: $color-primary-dim; |
|||
box-shadow: none; |
|||
background: $background; |
|||
color: $color-primary; |
|||
padding: 0 $size / 2 + 5px; |
|||
&::before { |
|||
border-top: $height / 2 solid $background; |
|||
border-bottom: $height / 2 solid $background; |
|||
} |
|||
&::after { |
|||
border-left: $size solid $background; |
|||
} |
|||
} |
|||
a { |
|||
padding: 5px 10px; |
|||
color: $color-primary-dim!important; |
|||
} |
|||
} |
|||
>li+li::before { |
|||
content: ''; |
|||
} |
|||
} |
@ -0,0 +1,180 @@ |
|||
.content-header h1 { |
|||
margin: 0; |
|||
} |
|||
|
|||
.box h1 { |
|||
font-size: 1.5em; |
|||
margin-top: 0; |
|||
margin-bottom: 20px; |
|||
} |
|||
|
|||
.row { |
|||
display: flex; |
|||
flex-wrap: wrap; |
|||
margin: 0; |
|||
.row { |
|||
margin: 0; |
|||
} |
|||
> div { |
|||
margin: 10px; |
|||
} |
|||
} |
|||
|
|||
.col { |
|||
display: flex; |
|||
flex-direction: column; |
|||
justify-content: center; |
|||
align-items: center; |
|||
} |
|||
|
|||
.total { |
|||
width: 100%; |
|||
display: flex; |
|||
flex-wrap: wrap; |
|||
} |
|||
|
|||
#trend-chart { |
|||
width: 80%; |
|||
} |
|||
|
|||
#total-pie { |
|||
width: 20%; |
|||
} |
|||
|
|||
.box { |
|||
margin: 20px; |
|||
} |
|||
|
|||
#stock-tip { |
|||
width: 350px; |
|||
} |
|||
|
|||
.overview-card, .line-chart, .data-table { |
|||
//@extend %shadow-soft; |
|||
box-shadow: 3px 6px 40px -6px rgba(125, 138, 179, 0.3); |
|||
} |
|||
|
|||
.overview-card { |
|||
flex-grow: 1; |
|||
display: flex; |
|||
justify-content: space-around; |
|||
align-items: center; |
|||
width: 200px; |
|||
padding: 20px 15px; |
|||
margin: 10px; |
|||
background: $color-panel; |
|||
border-radius: 10px; |
|||
img { |
|||
$size: 50px; |
|||
width: $size; |
|||
height: $size; |
|||
} |
|||
h1 { |
|||
font-size: 1em; |
|||
} |
|||
p.number { |
|||
font-size: 1.6em; |
|||
font-weight: bold; |
|||
color: #ef9131; |
|||
margin: 0; |
|||
} |
|||
} |
|||
|
|||
.total-display { |
|||
width: 100%; |
|||
margin: 0!important; |
|||
display: flex; |
|||
justify-content: space-around; |
|||
flex-wrap: wrap; |
|||
} |
|||
|
|||
.line-chart { |
|||
flex-grow: 1; |
|||
flex-shrink: 1; |
|||
max-width: 100%; |
|||
padding: 10px 20px!important; |
|||
} |
|||
|
|||
.line-chart, .data-table { |
|||
display: flex; |
|||
flex-direction: column; |
|||
padding: 20px; |
|||
background: $color-panel; |
|||
border-radius: 10px; |
|||
h1 { |
|||
font-size: 1.5em; |
|||
font-weight: bold; |
|||
} |
|||
.content { |
|||
display: flex; |
|||
flex-direction: column; |
|||
justify-content: center; |
|||
flex-grow: 1; |
|||
margin: 0; |
|||
padding: 0; |
|||
} |
|||
} |
|||
|
|||
.data-table { |
|||
table { |
|||
thead { |
|||
tr { |
|||
font-weight: bold; |
|||
td { |
|||
white-space: nowrap; |
|||
} |
|||
} |
|||
} |
|||
td { |
|||
padding: 8px; |
|||
font-size: .9em; |
|||
//white-space: nowrap; |
|||
overflow: hidden; |
|||
text-overflow: ellipsis; |
|||
&.line-number { |
|||
text-align: center; |
|||
} |
|||
&.highlight { |
|||
color: #ef9131; |
|||
} |
|||
} |
|||
} |
|||
.table-empty-text { |
|||
text-align: center; |
|||
font-size: 1.4em; |
|||
} |
|||
} |
|||
|
|||
.fill-space { |
|||
flex-grow: 1; |
|||
} |
|||
|
|||
@media (min-width: 1235px) { |
|||
.total-display { |
|||
flex-wrap: nowrap; |
|||
} |
|||
.row { |
|||
flex-wrap: nowrap; |
|||
width: 100%; |
|||
.line-chart { |
|||
//width: 60%; |
|||
flex: 1; |
|||
} |
|||
.data-table { |
|||
max-width: 400px; |
|||
flex-grow: 0; |
|||
flex-shrink: 0; |
|||
} |
|||
} |
|||
} |
|||
|
|||
@media (max-width: 1235px) { |
|||
.row { |
|||
flex-wrap: wrap; |
|||
.line-chart { |
|||
flex-grow: 1; |
|||
width: 98%; |
|||
padding-right: 2%; |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,183 @@ |
|||
html, body { |
|||
color: $color-fore; |
|||
background: $color-background!important; |
|||
font-family: -apple-system, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Helvetica Neue, Arial, sans-serif; |
|||
} |
|||
|
|||
body .fa { |
|||
font-family: "fa", "FontAwesome" !important; |
|||
} |
|||
|
|||
.content-wrapper { |
|||
margin-top: 70px; |
|||
margin-bottom: 10px; |
|||
} |
|||
.content-wrapper { |
|||
background: rgb(248, 250, 255)!important; |
|||
} |
|||
|
|||
input, select { |
|||
border-radius: 3px!important; |
|||
} |
|||
|
|||
button.btn-float, a.btn-float { |
|||
position: fixed; |
|||
display: block; |
|||
top: unset; |
|||
right: 50px; |
|||
bottom: 50px; |
|||
width: 60px; |
|||
height: 60px; |
|||
line-height: 60px; |
|||
padding: 0; |
|||
border-radius: 50%; |
|||
font-size: 1.6em; |
|||
box-shadow: 0 0 20px 5px rgba(0, 0, 0, .2); |
|||
z-index: 10000; |
|||
} |
|||
|
|||
.btn-info.btn-float { |
|||
bottom: 120px; |
|||
} |
|||
|
|||
fieldset { |
|||
transition: all .4s; |
|||
} |
|||
fieldset[disabled] { |
|||
opacity: 0; |
|||
cursor: auto!important; |
|||
} |
|||
|
|||
#driver-popover-item, #driver-highlighted-element-stage { |
|||
position: fixed!important; |
|||
} |
|||
|
|||
//.main-footer { |
|||
// position: absolute; |
|||
// bottom: 0; |
|||
// width: 100%; |
|||
//} |
|||
|
|||
::-webkit-scrollbar{width:6px!important;height:6px!important;} |
|||
body::-webkit-scrollbar{width:6px!important;height:6px!important;} |
|||
|
|||
::-webkit-scrollbar-track{background:rgba(200,200,200,0.22)!important;border-radius:8px!important;} |
|||
|
|||
|
|||
::-webkit-scrollbar-thumb{background-color: rgba(0, 0, 0, .1) !important;min-height:50px;border-radius:5px!important;} |
|||
|
|||
|
|||
@keyframes fadein { |
|||
0% {opacity: 0;} |
|||
100% {} |
|||
} |
|||
|
|||
.content-wrapper { |
|||
$margin: 20px; |
|||
margin-left: $sidebar-width + $margin!important; |
|||
margin-right: $margin; |
|||
} |
|||
|
|||
.main-footer { |
|||
margin-left: $sidebar-width!important; |
|||
a { |
|||
color: $color-primary-vivid; |
|||
} |
|||
} |
|||
|
|||
.tab-content { |
|||
height: auto!important; |
|||
border: none!important; |
|||
padding: 0!important; |
|||
margin-top: 30px; |
|||
} |
|||
|
|||
.login-logo { |
|||
font-weight: bold; |
|||
} |
|||
|
|||
.category-list { |
|||
.category-list-item { |
|||
&.category__hidden { |
|||
background: #f8f8f8; |
|||
color: #aaa; |
|||
} |
|||
> div { |
|||
display: flex; |
|||
align-items: center; |
|||
border: none!important; |
|||
} |
|||
.hidden-tip { |
|||
margin-left: 20px; |
|||
color: #aaa; |
|||
} |
|||
} |
|||
a.btn { |
|||
padding: 10px !important; |
|||
} |
|||
.btn-group { |
|||
a.btn { |
|||
border-radius: 0; |
|||
&:first-of-type { |
|||
border-radius: 5px 0 0 5px; |
|||
} |
|||
&:last-child { |
|||
border-radius: 0 5px 5px 0; |
|||
} |
|||
} |
|||
&.btn-group__circle { |
|||
a.btn { |
|||
padding: 8px!important; |
|||
margin: 0 5px; |
|||
font-size: .8em; |
|||
border-radius: 100%!important; |
|||
} |
|||
} |
|||
} |
|||
a.collapse-list-btn { |
|||
color: $color-primary; |
|||
i { |
|||
transition: transform .3s; |
|||
} |
|||
&[aria-expanded="true"] i { |
|||
transform: rotate(180deg); |
|||
} |
|||
&.disabled { |
|||
color: $color-dim; |
|||
cursor: default; |
|||
} |
|||
} |
|||
.drag-btn { |
|||
margin-left: 20px; |
|||
color: $color-info; |
|||
} |
|||
} |
|||
|
|||
.category-index { |
|||
min-height: 75vh; |
|||
} |
|||
|
|||
.goods-update .props-main-box { |
|||
position: relative!important; |
|||
top: 0; |
|||
left: 0; |
|||
margin-top: 20px; |
|||
} |
|||
|
|||
// 侧边栏折叠状态 |
|||
.sidebar-mini.sidebar-collapse .content-wrapper, |
|||
.sidebar-mini.sidebar-collapse .right-side, |
|||
.sidebar-mini.sidebar-collapse .main-footer { |
|||
margin-left: 70px!important; |
|||
} |
|||
|
|||
@media (max-width: 767px) { |
|||
body.sidebar-mini.sidebar-open { |
|||
.content-wrapper { |
|||
margin-left: 0!important; |
|||
} |
|||
} |
|||
.main-footer { |
|||
margin-left: 0!important; |
|||
} |
|||
} |
@ -0,0 +1,128 @@ |
|||
|
|||
//顶栏背景色 |
|||
.logo, .navbar-static-top { |
|||
background: none!important; |
|||
} |
|||
|
|||
.main-header { |
|||
position: fixed; |
|||
width: auto; |
|||
left: $sidebar-width; |
|||
right: 0; |
|||
display: flex; |
|||
//background-image: linear-gradient(45deg, $color-primary, adjust_hue($color-primary, 40)); |
|||
background: $color-panel; |
|||
box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.1)!important; |
|||
transition: all .3s ease-in-out; |
|||
z-index: 100 !important; |
|||
.logo-lg, .logo, .sidebar-toggle, .hidden-xs, .notifications-menu a.dropdown-toggle { |
|||
color: $color-thin !important; |
|||
} |
|||
.user-image { |
|||
background: lighten($color-primary, $lightenDegree); |
|||
} |
|||
.logo { |
|||
flex-grow: 1; |
|||
font-weight: bold; |
|||
} |
|||
.navbar-static-top { |
|||
margin-left: 0; |
|||
} |
|||
} |
|||
|
|||
.user-header { |
|||
background: $color-primary!important; |
|||
} |
|||
|
|||
.user-menu { |
|||
.dropdown-menu { |
|||
left: unset; |
|||
right: 0; |
|||
box-shadow: 0 4px 60px 10px rgba(0, 0, 0, 0.1)!important; |
|||
overflow: visible; |
|||
} |
|||
} |
|||
|
|||
.sidebar-toggle { |
|||
&:active, &:hover { |
|||
background: $color-pale!important; |
|||
} |
|||
} |
|||
.notifications-menu { |
|||
.label-warning { |
|||
display: block; |
|||
padding: 3px 5px!important; |
|||
font-size: .8em!important; |
|||
background: #ff4e3d !important; |
|||
border-radius: 100%; |
|||
} |
|||
.dropdown-menu { |
|||
width: auto!important; |
|||
box-shadow: 0 0 40px 10px rgba(0, 0, 0, 0.1)!important; |
|||
ul.menu { |
|||
max-height: 40vh!important; |
|||
li { |
|||
a { |
|||
display: flex!important; |
|||
align-items: center; |
|||
padding: 10px 20px!important; |
|||
span.number { |
|||
color: $color-danger; |
|||
margin: 0 10px; |
|||
font-weight: bold; |
|||
} |
|||
span.tip { |
|||
color: $color-danger; |
|||
margin-left: 10px; |
|||
font-weight: bold; |
|||
} |
|||
} |
|||
i { |
|||
width: auto!important; |
|||
font-size: 1.6em; |
|||
margin-right: 20px; |
|||
padding: 10px; |
|||
border-radius: 100%; |
|||
text-align: center; |
|||
&.blue { |
|||
$color: #5964ab; |
|||
background: $color; |
|||
color: lighten($color, 50%)!important; |
|||
} |
|||
&.yellow { |
|||
$color: #df8346; |
|||
background: $color; |
|||
color: lighten($color, 50%)!important; |
|||
} |
|||
&.red { |
|||
background: $color-danger; |
|||
color: lighten($color-danger, 50%)!important; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
body.sidebar-collapse { |
|||
.main-header { |
|||
left: 50px; |
|||
} |
|||
} |
|||
|
|||
@media (max-width: 767px) { |
|||
.main-header { |
|||
left: 0!important; |
|||
} |
|||
.logo { |
|||
padding: 0!important; |
|||
} |
|||
.main-header .logo, .main-header .navbar { |
|||
width: auto; |
|||
} |
|||
body.sidebar-mini.sidebar-open { |
|||
.main-header { |
|||
margin-left: 190px; |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,7 @@ |
|||
@import "./global"; |
|||
@import "sidebar"; |
|||
@import "widget"; |
|||
@import "header"; |
|||
@import "content-header"; |
|||
@import "dashboard"; |
|||
@import "amazing-creator"; |
@ -0,0 +1,193 @@ |
|||
|
|||
html { |
|||
background: #292d34; |
|||
background-image: url("/img/login_background.png") !important; |
|||
background-size: cover !important; |
|||
font-family: -apple-system, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Helvetica Neue, Arial, sans-serif; |
|||
} |
|||
|
|||
body { |
|||
position: fixed; |
|||
top: 0; |
|||
left: 0; |
|||
width: 100%; |
|||
height: 100%; |
|||
//background: rgba(0, 0, 0, .7)!important; |
|||
background: none !important; |
|||
} |
|||
|
|||
.login-box { |
|||
$color-primary: #2b457c; |
|||
$background: #e9f4ff; |
|||
|
|||
&__body { |
|||
position: fixed; |
|||
left: 50%; |
|||
top: 50%; |
|||
transform: translate(-50%, -50%); |
|||
display: flex; |
|||
height: 80vh; |
|||
box-shadow: 0 4px 30px 10px rgba(0, 0, 0, 0.2)!important; |
|||
//border-radius: 10px; |
|||
overflow: hidden; |
|||
} |
|||
&__bar { |
|||
position: absolute; |
|||
top: 40px; |
|||
width: 100%; |
|||
padding-right: 10px; |
|||
text-align: right; |
|||
border-left: 6px solid $background; |
|||
border-right: 6px solid $color-primary; |
|||
color: $color-primary; |
|||
font-weight: bold; |
|||
font-size: 1.5em; |
|||
} |
|||
&__left, &__right { |
|||
display: flex; |
|||
flex-direction: column; |
|||
justify-content: center; |
|||
align-items: center; |
|||
padding: 20px; |
|||
} |
|||
|
|||
&__left { |
|||
padding: 0 60px; |
|||
background: #1a1d25; |
|||
} |
|||
|
|||
&__logo { |
|||
display: flex; |
|||
flex-direction: column; |
|||
align-items: center; |
|||
justify-content: space-around; |
|||
|
|||
img { |
|||
$size: 150px; |
|||
width: $size; |
|||
height: $size; |
|||
margin-bottom: 40px; |
|||
} |
|||
|
|||
p { |
|||
font-size: 1.5em; |
|||
font-weight: bold; |
|||
color: #2ad0ff; |
|||
} |
|||
} |
|||
|
|||
&__right { |
|||
flex-grow: 1; |
|||
background: $background; |
|||
|
|||
.form-group.has-feedback { |
|||
position: relative; |
|||
|
|||
.form-control { |
|||
padding-left: 40px; |
|||
border: none; |
|||
border-bottom: 2px solid $color-primary; |
|||
background: none !important; |
|||
|
|||
&:-webkit-autofill { |
|||
-webkit-box-shadow: 0 0 0 1000px $background inset; |
|||
-webkit-text-fill-color: $color-primary; |
|||
} |
|||
} |
|||
|
|||
.form-control-feedback { |
|||
position: absolute; |
|||
left: 0; |
|||
color: $color-primary; |
|||
} |
|||
} |
|||
|
|||
.submit-area { |
|||
display: flex; |
|||
flex-direction: column; |
|||
align-content: center; |
|||
justify-content: space-around; |
|||
text-align: center; |
|||
.form-group.field-adminloginform-rememberme { |
|||
color: $color-primary; |
|||
|
|||
label { |
|||
font-weight: bold!important; |
|||
} |
|||
} |
|||
.btn.btn-primary.btn-block.btn-flat { |
|||
display: inline-block; |
|||
padding: 10px 20px; |
|||
border-radius: 20px; |
|||
background: $color-primary; |
|||
box-shadow: 0 5px 10px 0 rgba(0, 0, 0, .2); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
@media (min-width: 1200px) { |
|||
.login-box__body { |
|||
width: 60vw; |
|||
} |
|||
} |
|||
|
|||
@media (max-width: 1200px) and (min-width: 800px) { |
|||
.login-box__body { |
|||
width: 700px; |
|||
} |
|||
} |
|||
|
|||
@media (max-width: 800px) and (min-width: 400px) { |
|||
.login-box__body { |
|||
width: 87.5%; |
|||
.login-box__left { |
|||
padding: 0 30px; |
|||
} |
|||
} |
|||
} |
|||
|
|||
@media (max-width: 600px) { |
|||
.login-box { |
|||
&__body { |
|||
flex-direction: column; |
|||
width: 100%; |
|||
height: 100%; |
|||
} |
|||
&__left { |
|||
padding: 20px 0; |
|||
background: #1a1d25; |
|||
} |
|||
&__logo { |
|||
display: flex; |
|||
flex-direction: row; |
|||
align-items: center; |
|||
justify-content: space-around; |
|||
|
|||
img { |
|||
$size: 50px; |
|||
width: $size; |
|||
height: $size; |
|||
margin-bottom: 0; |
|||
margin-right: 20px; |
|||
} |
|||
|
|||
p { |
|||
margin-bottom: 0; |
|||
font-size: 1.5em; |
|||
font-weight: bold; |
|||
color: #6fd1ff; |
|||
} |
|||
} |
|||
&__bar { |
|||
top: 110px; |
|||
width: auto; |
|||
left: 50%; |
|||
padding: 0 10px 2px 10px; |
|||
transform: translateX(-50%); |
|||
border: none; |
|||
border-bottom: 4px solid $color-primary; |
|||
text-align: center; |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,201 @@ |
|||
.main-sidebar { |
|||
position: fixed; |
|||
width: $sidebar-width; |
|||
height: 100%; |
|||
padding-top: 20px!important; |
|||
margin-bottom: 100px; |
|||
overflow-y: auto; |
|||
background: $color-sidebar!important; |
|||
//background-image: linear-gradient(135deg, #3a345d, #2d2a46); |
|||
.sidebar__header { |
|||
display: flex; |
|||
flex-direction: column; |
|||
align-items: center; |
|||
img { |
|||
$size: 60px; |
|||
width: $size; |
|||
height: $size; |
|||
border-radius: 50%; |
|||
} |
|||
p { |
|||
color: $color-sidebar-logo; |
|||
font-style: italic; |
|||
margin-top: 15px; |
|||
font-size: 1.2em; |
|||
font-weight: bold; |
|||
} |
|||
} |
|||
.user-panel .info { |
|||
a { |
|||
color: lighten($color-thin, 20%); |
|||
} |
|||
i { |
|||
font-size: 10px; |
|||
} |
|||
} |
|||
.sidebar-form { |
|||
border: none !important; |
|||
.input-group { |
|||
border-radius: 40px; |
|||
overflow: hidden; |
|||
input { |
|||
border: none !important; |
|||
} |
|||
} |
|||
input.form-control[type="text"], #search-btn { |
|||
background: lighten($color-primary-pale, 5%)!important; |
|||
color: $color-primary-dim !important; |
|||
} |
|||
} |
|||
ul.sidebar-menu { |
|||
margin-top: 10px; |
|||
> li.treeview { |
|||
// 一级菜单 |
|||
list-style: none; |
|||
margin-left: 15px; |
|||
a { |
|||
background: $color-sidebar!important; |
|||
color: $color-sidebar-fore; |
|||
&:hover { |
|||
color: $color-sidebar-active-fore!important; |
|||
} |
|||
} |
|||
> a { |
|||
padding: 10px 15px!important; |
|||
font-weight: normal; |
|||
font-size: 1em; |
|||
.fa { |
|||
margin-right: 5px; |
|||
font-size: 1.2em; |
|||
} |
|||
} |
|||
ul.treeview-menu { |
|||
padding-left: 0; |
|||
padding-bottom: 10px; |
|||
background: $color-sidebar!important; |
|||
li { |
|||
// 二级菜单 |
|||
padding: 5px 0; |
|||
//border-left: 4px solid transparent; |
|||
background: $color-sidebar!important; |
|||
&:hover { |
|||
background: $color-pale; |
|||
} |
|||
a { |
|||
padding: 0; |
|||
padding-left: 48px; |
|||
font-size: .9em; |
|||
background: $color-sidebar!important; |
|||
} |
|||
// 隐藏二级菜单前面的 O |
|||
.fa-circle-o { |
|||
display: none; |
|||
} |
|||
} |
|||
} |
|||
&.active { |
|||
// 当前页面所在的一级菜单 |
|||
background: $color-sidebar-active!important; |
|||
border-radius: 20px 0 0 20px; |
|||
overflow: hidden; |
|||
> a { |
|||
//color: $color-primary-vivid !important; |
|||
background: $color-sidebar-active!important; |
|||
color: $color-sidebar-active-fore!important; |
|||
} |
|||
.treeview-menu { |
|||
background: $color-sidebar-active!important; |
|||
li { |
|||
background: $color-sidebar-active!important; |
|||
a { |
|||
background: $color-sidebar-active!important; |
|||
} |
|||
&.active { |
|||
// 选中的二级菜单 |
|||
//border-left-color: $color-primary; |
|||
background: $color-sidebar-active!important; |
|||
a { |
|||
color: $color-sidebar-active-fore !important; |
|||
font-weight: normal; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
&.menu-open { |
|||
// 展开的一级菜单 |
|||
//color: $color-primary !important; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
body.sidebar-collapse { |
|||
.main-sidebar { |
|||
padding-top: 10px!important; |
|||
overflow: visible; |
|||
} |
|||
.sidebar__header { |
|||
display: flex; |
|||
flex-direction: column; |
|||
align-items: center; |
|||
img { |
|||
$size: 25px; |
|||
width: $size; |
|||
height: $size; |
|||
border-radius: 50%; |
|||
transition: all .3s ease-in-out; |
|||
} |
|||
p { |
|||
display: none; |
|||
font-size: 12px; |
|||
margin-top: 5px; |
|||
margin-bottom: 0; |
|||
transition: all .3s ease-in-out; |
|||
} |
|||
} |
|||
ul.sidebar-menu { |
|||
> li.treeview { |
|||
margin-left: 0; |
|||
overflow: visible!important; |
|||
a { |
|||
border-radius: 50%; |
|||
} |
|||
//border-radius: 50%!important; |
|||
//a { |
|||
// padding: 10px!important; |
|||
// i { |
|||
// margin: 0!important; |
|||
// } |
|||
//} |
|||
a span { |
|||
width: 178px!important; |
|||
} |
|||
.pull-right-container { |
|||
margin-top: -1px!important; |
|||
padding: 8px 3px!important; |
|||
} |
|||
.treeview-menu { |
|||
left: 48px!important; |
|||
top: unset!important; |
|||
border: none!important; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
.main-sidebar, .main-footer { |
|||
box-shadow: 0 4px 30px 0 rgba(223, 225, 230, 0.5) !important; |
|||
border: none !important; |
|||
} |
|||
|
|||
.panel { |
|||
padding: 10px; |
|||
margin: 0; |
|||
} |
|||
|
|||
@media (max-width: 767px) { |
|||
.content-wrapper { |
|||
margin-left: 30px!important; |
|||
} |
|||
} |
@ -0,0 +1,30 @@ |
|||
$color-primary: #2f4887; |
|||
$color-primary-vivid: hsl(hue($color-primary), saturation($color-primary), 50%); |
|||
$color-primary-dim: lighten($color-primary-vivid, 35%); |
|||
$color-primary-pale: lighten($color-primary-vivid, 42%); |
|||
$color-background: rgb(248, 250, 255); |
|||
$color-panel: #fff; |
|||
$color-pale: #f6f6f6; |
|||
$color-fore: #666; |
|||
$color-dim: #b4b6c5; |
|||
$color-thin: #515156; |
|||
$color-sidebar: #080e39; |
|||
$color-sidebar-active: #2a2e52; |
|||
$color-sidebar-fore: #dce0f2; |
|||
$color-sidebar-active-fore: #26caff; |
|||
$color-sidebar-logo: #26caff; |
|||
|
|||
$color-success: #2ab57d; |
|||
$color-danger: #c60013; |
|||
$color-info: #2a92ec; |
|||
|
|||
$lightenDegree: 70%; |
|||
$sidebar-width: 190px; |
|||
|
|||
%shadow { |
|||
box-shadow: 0 0 30px 0 rgba(0, 0, 0, 0.1)!important; |
|||
} |
|||
|
|||
%shadow-soft { |
|||
box-shadow: 0 4px 30px 0 rgba(223, 225, 230, 0.5) !important; |
|||
} |
@ -0,0 +1,301 @@ |
|||
.content-wrapper { |
|||
margin-left: 30px; |
|||
margin-right: 30px; |
|||
> section.content { |
|||
margin: 30px 0; |
|||
background: $color-panel!important; |
|||
box-shadow: 0 4px 30px 2px rgba(223, 225, 230, 0.5)!important; |
|||
border-radius: 10px; |
|||
} |
|||
} |
|||
|
|||
.error-page .fa-warning { |
|||
font-size: 1.5em; |
|||
} |
|||
|
|||
#treeView { |
|||
background: none; |
|||
} |
|||
|
|||
body a:focus { |
|||
text-decoration: none; |
|||
} |
|||
|
|||
.box, .panel { |
|||
padding: 0!important; |
|||
margin: 0!important; |
|||
border: none!important; |
|||
border-image-width: 0!important; |
|||
box-shadow: none!important; |
|||
//box-shadow: 0 4px 30px 2px rgba(223, 225, 230, 0.5)!important; |
|||
overflow: hidden; |
|||
.panel { |
|||
box-shadow: none!important; |
|||
} |
|||
} |
|||
|
|||
.grid-view { |
|||
margin-top: 20px; |
|||
} |
|||
|
|||
.box.box-default { |
|||
margin: 0; |
|||
} |
|||
|
|||
.order-index { |
|||
.panel-default { |
|||
padding: 0; |
|||
padding-top: 20px; |
|||
margin: 0; |
|||
} |
|||
} |
|||
|
|||
.panel { |
|||
margin: 20px 0; |
|||
.panel-heading { |
|||
padding: 0; |
|||
} |
|||
} |
|||
|
|||
.btn { |
|||
border: none; |
|||
$btn-list: (name: default, color: $color-pale), |
|||
(name: primary, color: $color-primary), |
|||
(name: success, color: $color-success), |
|||
(name: info, color: $color-info), |
|||
(name: danger, color: $color-danger); |
|||
|
|||
@each $btn in $btn-list { |
|||
&.btn-#{map_get($btn, name)} { |
|||
$color: map_get($btn, color); |
|||
background: $color; |
|||
@if (lightness($color) < 70%) { |
|||
color: lighten($color, $lightenDegree)!important; |
|||
} @else { |
|||
color: darken($color, $lightenDegree)!important; |
|||
} |
|||
&:hover { |
|||
background: darken(map_get($btn, color), 5%); |
|||
} |
|||
&:active { |
|||
background: darken(map_get($btn, color), 10%); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
.dropdown-toggle { |
|||
box-shadow: none!important; |
|||
} |
|||
.select-data-box { |
|||
position: static!important; |
|||
} |
|||
|
|||
.panel-heading, .panel-footer { |
|||
background: $color-panel!important; |
|||
} |
|||
|
|||
.panel-heading { |
|||
border: none; |
|||
} |
|||
|
|||
.nav-tabs { |
|||
display: flex; |
|||
border-bottom-color: $color-pale; |
|||
li { |
|||
float: none; |
|||
border-bottom: 3px solid transparent; |
|||
a { |
|||
color: $color-fore; |
|||
} |
|||
a, a:hover { |
|||
border: none!important; |
|||
} |
|||
i { |
|||
color: $color-primary-vivid!important; |
|||
} |
|||
&.active { |
|||
border-bottom-color: $color-primary; |
|||
box-shadow: none; |
|||
a { |
|||
margin-right: 0; |
|||
color: $color-primary; |
|||
font-weight: bold; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
.kv-panel-before, .kv-panel-after, .panel-footer { |
|||
border: none; |
|||
} |
|||
|
|||
table.kv-grid-table { |
|||
&, thead, th, td { |
|||
border: none!important; |
|||
} |
|||
thead { |
|||
background: lighten($color-primary-pale, 5%); |
|||
a { |
|||
color: $color-primary; |
|||
} |
|||
input.form-control[type][name], select { |
|||
background: lighten($color-primary-pale, 10%)!important; |
|||
} |
|||
} |
|||
>tbody { |
|||
>tr:nth-of-type(2n) { |
|||
background: lighten($color-primary-pale, 6%); |
|||
&:hover { |
|||
background: lighten($color-primary-pale, 6%)!important; |
|||
} |
|||
} |
|||
>tr:hover { |
|||
background: $color-panel!important; |
|||
} |
|||
} |
|||
td { |
|||
vertical-align: middle!important; |
|||
} |
|||
} |
|||
|
|||
input:not([type="submit"]):not([type="button"]):not([type="reset"]):not([class^="ant-input"]), select { |
|||
width: 100%; |
|||
padding: 6px 12px; |
|||
background: lighten($color-primary-pale, 3%) !important; |
|||
//background: $color-pale!important; |
|||
//border: 1.5px solid lighten($color-primary-dim, 10%)!important; |
|||
border: none!important; |
|||
border-radius: 5px!important; |
|||
color: darken($color-primary, 10%); |
|||
box-shadow: none!important; |
|||
} |
|||
|
|||
input.file-caption-name { |
|||
background: none!important; |
|||
} |
|||
|
|||
.input-group-addon { |
|||
background: lighten($color-primary-pale, 5%)!important; |
|||
border: none!important; |
|||
&:hover { |
|||
background: $color-primary-pale!important; |
|||
} |
|||
} |
|||
|
|||
.select2-selection.select2-selection--multiple, |
|||
.select2-dropdown.select2-dropdown--below { |
|||
border: none!important; |
|||
} |
|||
|
|||
::-webkit-input-placeholder { |
|||
color: lighten($color-primary, 50%)!important; |
|||
//color: $color-dim!important; |
|||
} |
|||
|
|||
// 复选框 |
|||
.cbx-active { |
|||
border: 2px solid $color-primary-dim!important; |
|||
box-shadow: none!important; |
|||
border-radius: 0; |
|||
.cbx-icon { |
|||
background: lighten($color-primary-pale, 5%); |
|||
i { |
|||
color: $color-primary-vivid!important; |
|||
} |
|||
} |
|||
} |
|||
|
|||
// Switch |
|||
label.btn.btn-default { |
|||
box-shadow: none!important; |
|||
&.active { |
|||
background: darken($color-primary-vivid, 10%); |
|||
color: $color-primary-pale; |
|||
} |
|||
} |
|||
|
|||
.props-main-box, .props-detail-box { |
|||
position: static!important; |
|||
width: 100%!important; |
|||
} |
|||
|
|||
.tab-content { |
|||
min-height: 350px; |
|||
} |
|||
|
|||
.goods-index { |
|||
td:last-child { |
|||
font-size: 0; |
|||
.btn { |
|||
padding: 10px; |
|||
border-radius: 0; |
|||
} |
|||
} |
|||
} |
|||
|
|||
ul.pagination { |
|||
li { |
|||
a, span { |
|||
border: none!important; |
|||
margin: 0 5px; |
|||
border-radius: 100%!important; |
|||
} |
|||
&.active { |
|||
a { |
|||
background: $color-primary-vivid; |
|||
//box-shadow: 0 0 20px 2px rgba(0, 0, 0, 0.2)!important; |
|||
&:hover { |
|||
background: darken($color-primary-vivid, 10%); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
.after-sale-index .panel, .panel-default { |
|||
position: relative; |
|||
min-height: 75vh; |
|||
} |
|||
|
|||
$width: 95px; |
|||
|
|||
.rc-switch { |
|||
width: $width!important; |
|||
margin-bottom: 10px; |
|||
&[aria-checked="false"] { |
|||
background: $color-info; |
|||
} |
|||
&[aria-checked="true"] { |
|||
background: $color-success; |
|||
} |
|||
} |
|||
|
|||
.rc-switch-checked:after { |
|||
left: $width - 22px!important; |
|||
} |
|||
|
|||
.empty { |
|||
position: absolute; |
|||
display: block; |
|||
width: 100%; |
|||
left: 50%; |
|||
top: 60%; |
|||
transform: translate(-50%, -50%); |
|||
text-align: center; |
|||
font-size: 2em; |
|||
} |
|||
|
|||
.kv-page-summary-container td { |
|||
padding: 0!important; |
|||
} |
|||
|
|||
table.table.table-striped.table-bordered.detail-view { |
|||
&, tbody, tr, th, td { |
|||
border: none!important; |
|||
} |
|||
th { |
|||
padding-right: 50px; |
|||
white-space: nowrap; |
|||
} |
|||
} |
@ -0,0 +1,76 @@ |
|||
import axios from 'axios' |
|||
import {message} from 'antd'; |
|||
|
|||
// 请求拦截器
|
|||
axios.interceptors.request.use(config => ({ |
|||
...config, |
|||
data: { |
|||
...config.data, |
|||
'_csrf-api': window.csrfToken |
|||
} |
|||
})); |
|||
|
|||
// 响应拦截器
|
|||
axios.interceptors.response.use(function (response) { |
|||
const rawData = response && response.data; |
|||
let data = null; |
|||
const errorText = '服务器返回的数据格式不正确'; |
|||
|
|||
if (rawData && typeof response.data === 'object') { |
|||
// 若返回的数据是对象或者数组
|
|||
data = rawData; |
|||
} else if (typeof response.data === 'string') { |
|||
// 若返回的数据是字符串,尝试将其按 JSON 格式转为对象
|
|||
try { |
|||
data = JSON.parse(rawData); |
|||
} catch (e) { |
|||
message.error(errorText); |
|||
return Promise.reject(errorText); |
|||
} |
|||
} else { |
|||
message.error(errorText); |
|||
return Promise.reject(errorText); |
|||
} |
|||
|
|||
if (data) { |
|||
const {status, info} = response.data; |
|||
|
|||
if (status === undefined || status === true || status === 1) { |
|||
return response.data; |
|||
} else if (info) { |
|||
const errorText = info; |
|||
message.error(errorText); |
|||
console.error(errorText); |
|||
return Promise.reject(errorText) |
|||
} else { |
|||
const errorText = `操作失败`; |
|||
message.error(errorText); |
|||
console.error(errorText); |
|||
return Promise.reject(errorText) |
|||
} |
|||
} else if (response) { |
|||
return response; |
|||
} else { |
|||
return Promise.reject(`服务器无响应`); |
|||
} |
|||
}, function (error) { |
|||
let errorText = ''; |
|||
|
|||
if (error && error.response && error.response.status === 500) { |
|||
if (error.response.data) { |
|||
errorText = error.response.data.message || error.response.data; |
|||
} else { |
|||
errorText = error.response.message |
|||
} |
|||
} else if (error.response && error.response.status) { |
|||
errorText = `请求出错:${error.response.statusText}(${error.response.status})` |
|||
} else if (error.message) { |
|||
errorText = error.message |
|||
} else { |
|||
errorText = error |
|||
} |
|||
|
|||
errorText && message.error(errorText); |
|||
error && console.error(error); |
|||
return Promise.reject(errorText); |
|||
}); |
@ -0,0 +1,84 @@ |
|||
const path = require('path'); |
|||
const webpack = require('webpack'); |
|||
const HtmlWebpackPlugin = require('html-webpack-plugin'); |
|||
const CleanWebpackPlugin = require('clean-webpack-plugin'); |
|||
|
|||
const entryList = { |
|||
dashboard: './src/dashboard/index.js', |
|||
style: './src/styles/index.scss', |
|||
login_style: './src/styles/login.scss', |
|||
order_detail: './src/detail-display/index.js', |
|||
sku: './src/sku/index.js', |
|||
mini_program_management: './src/mini-program-management/index.js', |
|||
spread: './src/spread/index.js', |
|||
sku_item: './src/sku-item/index.js', |
|||
'custom-menu': './src/custom-menu/index.js', |
|||
sku_for_activity: './src/sku-for-activity/index.js', |
|||
}; |
|||
|
|||
const needIconfontEntries = ['style', 'login_style']; |
|||
|
|||
module.exports = function (env, argv) { |
|||
const pathToClean = argv.module ? `${argv.module}.*.js` : 'custom'; |
|||
const entry = argv.module ? {[argv.module]: entryList[argv.module]} : entryList; |
|||
const htmlWebpackPluginList = Object.keys(entry).map(entryItem => new HtmlWebpackPlugin({ |
|||
filename: `${entryItem}.html`, |
|||
chunks: [entryItem], |
|||
template: needIconfontEntries.includes(entryItem) ? 'src/iconfont.html' : 'src/import.html' |
|||
})); |
|||
|
|||
return { |
|||
entry, |
|||
output: { |
|||
filename: '[name].[chunkhash].js', |
|||
path: path.resolve(__dirname, 'custom'), |
|||
publicPath: '/custom/' |
|||
}, |
|||
module: { |
|||
rules: [ |
|||
{ |
|||
test: /\.js$/, |
|||
exclude: /(node_modules|bower_components)/, |
|||
use: { |
|||
loader: 'babel-loader' |
|||
} |
|||
}, |
|||
{ |
|||
test: /\.scss$/, |
|||
use: ["style-loader", "css-loader", "resolve-url-loader", "sass-loader?sourceMap", |
|||
{ |
|||
loader: 'sass-resources-loader', |
|||
options: { |
|||
resources: './src/styles/variables.scss' |
|||
} |
|||
}] |
|||
}, |
|||
{ |
|||
test: /\.css$/, |
|||
use: ["style-loader", "css-loader"] |
|||
}, |
|||
{ |
|||
test: /\.(png|jpg|gif|eot|svg|ttf|woff2?)(\?.*)?$/i, |
|||
use: [ |
|||
{ |
|||
loader: 'url-loader', |
|||
options: { |
|||
limit: 10000 |
|||
} |
|||
}, |
|||
'file-loader' |
|||
] |
|||
} |
|||
] |
|||
}, |
|||
plugins: [ |
|||
new CleanWebpackPlugin({ |
|||
cleanOnceBeforeBuildPatterns: [pathToClean] |
|||
}), |
|||
new webpack.DefinePlugin({ |
|||
ENV_DEV: JSON.stringify(argv.mode === 'development') |
|||
}), |
|||
...htmlWebpackPluginList, |
|||
] |
|||
}; |
|||
}; |
Some files were not shown because too many files changed in this diff
Write
Preview
Loading…
Cancel
Save
Reference in new issue