quanwei
2025-10-28 36cacbaf78e510713002fcd5e3d61cece2e01421
名片模板
41 files added
7471 ■■■■■ changed files
admin/app/api/controller/plus/business/Business.php 17 ●●●●● patch | view | raw | blame | history
admin/app/api/controller/plus/business/Grade.php 17 ●●●●● patch | view | raw | blame | history
admin/app/api/controller/plus/business/Industry.php 58 ●●●●● patch | view | raw | blame | history
admin/app/api/controller/plus/business/Saving.php 17 ●●●●● patch | view | raw | blame | history
admin/app/api/controller/plus/business/Template.php 17 ●●●●● patch | view | raw | blame | history
admin/app/api/model/plus/business/Business.php 7 ●●●●● patch | view | raw | blame | history
admin/app/api/model/plus/business/Grade.php 7 ●●●●● patch | view | raw | blame | history
admin/app/api/model/plus/business/Industry.php 9 ●●●●● patch | view | raw | blame | history
admin/app/api/model/plus/business/Saving.php 10 ●●●●● patch | view | raw | blame | history
admin/app/api/model/plus/business/Template.php 7 ●●●●● patch | view | raw | blame | history
admin/app/common/model/plus/business/Business.php 93 ●●●●● patch | view | raw | blame | history
admin/app/common/model/plus/business/Grade.php 104 ●●●●● patch | view | raw | blame | history
admin/app/common/model/plus/business/Industry.php 127 ●●●●● patch | view | raw | blame | history
admin/app/common/model/plus/business/Saving.php 52 ●●●●● patch | view | raw | blame | history
admin/app/common/model/plus/business/Template.php 36 ●●●●● patch | view | raw | blame | history
admin/app/common/service/business/Poster.php 431 ●●●●● patch | view | raw | blame | history
admin/app/shop/controller/plus/business/Business.php 30 ●●●●● patch | view | raw | blame | history
admin/app/shop/controller/plus/business/Grade.php 79 ●●●●● patch | view | raw | blame | history
admin/app/shop/controller/plus/business/Industry.php 115 ●●●●● patch | view | raw | blame | history
admin/app/shop/controller/plus/business/Template.php 195 ●●●●● patch | view | raw | blame | history
admin/app/shop/model/plus/business/Business.php 12 ●●●●● patch | view | raw | blame | history
admin/app/shop/model/plus/business/Grade.php 96 ●●●●● patch | view | raw | blame | history
admin/app/shop/model/plus/business/Industry.php 75 ●●●●● patch | view | raw | blame | history
admin/app/shop/model/plus/business/Template.php 12 ●●●●● patch | view | raw | blame | history
mobile/pages/plus/business/add.vue 495 ●●●●● patch | view | raw | blame | history
mobile/pages/plus/business/components/visit-card.vue 176 ●●●●● patch | view | raw | blame | history
mobile/pages/plus/business/detail.vue 448 ●●●●● patch | view | raw | blame | history
mobile/pages/plus/business/index.vue 507 ●●●●● patch | view | raw | blame | history
mobile/pages/plus/business/information.vue 437 ●●●●● patch | view | raw | blame | history
mobile/pages/plus/business/share.vue 390 ●●●●● patch | view | raw | blame | history
mobile/pages/plus/business/visitors.vue 288 ●●●●● patch | view | raw | blame | history
shop_vue/src/api/business.js 55 ●●●●● patch | view | raw | blame | history
shop_vue/src/views/plus/business/business/index.vue 7 ●●●●● patch | view | raw | blame | history
shop_vue/src/views/plus/business/grade/index.vue 278 ●●●●● patch | view | raw | blame | history
shop_vue/src/views/plus/business/index.vue 135 ●●●●● patch | view | raw | blame | history
shop_vue/src/views/plus/business/industry/Add.vue 104 ●●●●● patch | view | raw | blame | history
shop_vue/src/views/plus/business/industry/Edit.vue 108 ●●●●● patch | view | raw | blame | history
shop_vue/src/views/plus/business/industry/index.vue 179 ●●●●● patch | view | raw | blame | history
shop_vue/src/views/plus/business/template/add.vue 993 ●●●●● patch | view | raw | blame | history
shop_vue/src/views/plus/business/template/edit.vue 1014 ●●●●● patch | view | raw | blame | history
shop_vue/src/views/plus/business/template/index.vue 234 ●●●●● patch | view | raw | blame | history
admin/app/api/controller/plus/business/Business.php
New file
@@ -0,0 +1,17 @@
<?php
namespace app\api\controller\plus\business;
use app\api\controller\Controller;
use app\api\model\plus\business\Business as BusinessModel;
class Business extends Controller
{
    /**
     * 获取名片列表
     */
    public function getList()
    {
        $model = new BusinessModel();
        return $this->renderSuccess('',$model->getLists());
    }
}
admin/app/api/controller/plus/business/Grade.php
New file
@@ -0,0 +1,17 @@
<?php
namespace app\api\controller\plus\business;
use app\api\controller\Controller;
use app\api\model\plus\business\Grade as GradeModel;
class Grade extends Controller
{
    /**
     * 获取等级列表
     */
    public function getList()
    {
        $model = new GradeModel();
        return $this->renderSuccess('',$model->getLists());
    }
}
admin/app/api/controller/plus/business/Industry.php
New file
@@ -0,0 +1,58 @@
<?php
namespace app\api\controller\plus\business;
use app\api\controller\Controller;
use app\api\model\plus\business\Industry as IndustryModel;
class Industry extends Controller
{
    /**
     * 获取所有行业(树状结构)
     */
    public function getIndustryTree()
    {
        $tree = IndustryModel::getCacheTree();
        return $this->renderSuccess(compact('tree'));
    }
    /**
     * 获取所有行业列表
     */
    public function getIndustryList()
    {
        $list = IndustryModel::getCacheAll();
        return $this->renderSuccess(compact('list'));
    }
    /**
     * 获取行业详情
     */
    public function detail($industry_id)
    {
        $industry = IndustryModel::detail($industry_id);
        if (!$industry) {
            return $this->renderError('行业不存在');
        }
        return $this->renderSuccess(compact('industry'));
    }
    /**
     * 获取一级行业列表
     */
    public function getFirstIndustry()
    {
        $list = IndustryModel::getFirstIndustry();
        return $this->renderSuccess(compact('list'));
    }
    /**
     * 根据上级ID获取子行业
     */
    public function getSubIndustry($parent_id = 0)
    {
        $model = new IndustryModel;
        $list = $model->where('parent_id', '=', $parent_id)->where('status', '=', 1)
            ->order(['sort' => 'asc', 'create_time' => 'asc'])->select();
        return $this->renderSuccess(compact('list'));
    }
}
admin/app/api/controller/plus/business/Saving.php
New file
@@ -0,0 +1,17 @@
<?php
namespace app\api\controller\plus\business;
use app\api\controller\Controller;
use app\api\model\plus\business\Saving as SavingModel;
class Saving extends Controller
{
    /**
     * 获取名片记录列表
     */
    public function getList()
    {
        $model = new SavingModel();
        return $this->renderSuccess('',$model->getLists());
    }
}
admin/app/api/controller/plus/business/Template.php
New file
@@ -0,0 +1,17 @@
<?php
namespace app\api\controller\plus\business;
use app\api\controller\Controller;
use app\api\model\plus\business\Template as TemplateModel;
class Template extends Controller
{
    /**
     * 获取模板列表
     */
    public function getList()
    {
        $model = new TemplateModel();
        return $this->renderSuccess('',$model->getLists());
    }
}
admin/app/api/model/plus/business/Business.php
New file
@@ -0,0 +1,7 @@
<?php
namespace app\api\model\plus\business;
use app\common\model\plus\business\Business as CommonBusiness;
class Business extends CommonBusiness
{
}
admin/app/api/model/plus/business/Grade.php
New file
@@ -0,0 +1,7 @@
<?php
namespace app\api\model\plus\business;
use app\common\model\plus\business\Grade as CommonGrade;
class Grade extends CommonGrade
{
}
admin/app/api/model/plus/business/Industry.php
New file
@@ -0,0 +1,9 @@
<?php
namespace app\api\model\plus\business;
use app\common\model\plus\business\Industry as CommonIndustry;
class Industry extends CommonIndustry
{
}
admin/app/api/model/plus/business/Saving.php
New file
@@ -0,0 +1,10 @@
<?php
namespace app\api\model\plus\business;
use app\common\model\plus\business\Saving as CommonSaving;
/**
 * 名片记录模型
 */
class Saving extends CommonSaving
{
}
admin/app/api/model/plus/business/Template.php
New file
@@ -0,0 +1,7 @@
<?php
namespace app\api\model\plus\business;
use app\common\model\plus\business\Template as CommonTemplate;
class Template extends CommonTemplate
{
}
admin/app/common/model/plus/business/Business.php
New file
@@ -0,0 +1,93 @@
<?php
namespace app\common\model\plus\business;
use app\common\model\BaseModel;
/**
 * 名片管理模型
 */
class Business extends BaseModel
{
    protected $name='business_card';
    public function getSexAttr($value){
        $data=[10=>'未知',20=>'男',30=>'女'];
        return $data[$value];
    }
    /**
     * 关联用户
     * @return \think\model\relation\HasOne
     */
    public function user(){
        $model=self::getCalledModule()?:'common';
        return $this->hasOne("app\\$model\\model\\user\\User",'user_id','user_id');
    }
    /**
     * 头像
     * @return \think\model\relation\HasOne
     */
    public function image(){
        $model=self::getCalledModule()?:'common';
        return $this->hasOne("app\\$model\\model\\file\\UploadFile",'file_id','file_id')->bind(['file_path']);
    }
    /**
     * logo
     * @return \think\model\relation\HasOne
     */
    public function logoImage(){
        $model=self::getCalledModule()?:'common';
        return $this->hasOne("app\\$model\\model\\file\\UploadFile",'file_id','logo')->bind(['file_path']);
    }
    /**
     * 添加
     * @param $data
     * @return false|int
     */
    public function add($data){
        $data['app_id']=self::$app_id;
        return $this->save($data);
    }
    public function getUnitAttr($name)
    {
        return json_decode($name);
    }
    public function getDutiesAttr($name)
    {
        return json_decode($name);
    }
    public function getAddressAttr($name)
    {
        return json_decode($name);
    }
    public function getPositionAttr($name)
    {
        return $name?json_decode($name):[];
    }
    /**
     * 获取名片列表
     * @param $param
     * @return \think\Paginator
     * @throws \think\db\exception\DbException
     */
    public function getList($param=[]){
        $paramr=array_merge(['listRow'=>15],$param);
        $where=[];
        !empty($paramr['name'])&&$where['name']=['like','%'.$paramr['name'].'%'];
        !empty($paramr['search'])&&$where['name|duties|unit']=['like','%'.$paramr['search'].'%'];
        !empty($paramr['user_id'])&&$where['user_id']=$paramr['user_id'];
        if(!empty($paramr['sort'])){
            if($paramr['sort']=='name'){
                $order=['name'=>"asc"];
            }else if($paramr['sort']=='time'){
                $order=['create_time'=>"asc"];
            }else{
                $order=['unit'=>"asc"];
            }
        }else{
            $order=['is_default'=>'desc','create_time'=>'desc'];
        }
        return $this->with(['user','image','logoImage'])->order($order)->where($where)->paginate($paramr);
    }
}
admin/app/common/model/plus/business/Grade.php
New file
@@ -0,0 +1,104 @@
<?php
namespace app\common\model\plus\business;
use app\common\model\BaseModel;
/**
 * 名片等级模型
 */
class Grade extends BaseModel
{
    protected $pk = 'grade_id';
    protected $name = 'business_card_grade';
    /**
     * 名片等级模型初始化
     */
    public static function init()
    {
        parent::init();
    }
    /**
     * 获取详情
     */
    public static function detail($grade_id)
    {
        return (new static())->find($grade_id);
    }
    /**
     * 获取列表记录
     */
    public function getLists()
    {
        return $this->where('app_id', '=', self::$app_id)
            ->order(['weight' => 'asc', 'create_time' => 'asc'])
            ->select();
    }
    /**
     * 查询列表
     */
    public function selectList()
    {
        return $this->where('app_id', '=', self::$app_id)
            ->order(['weight' => 'asc', 'create_time' => 'asc'])
            ->select();
    }
    /**
     * 获取默认等级id
     */
    public static function getDefaultGradeId()
    {
        $model = new static();
        $grade = $model->where('app_id', '=', self::$app_id)->where('weight', '=', 1)->find();
        if (!$grade) {
            $model->save([
                'name' => '默认等级',
                'price' => 0.00,
                'weight' => 1,
                'app_id' => self::$app_id
            ]);
            $grade_id = $model['grade_id'];
        } else {
            $grade_id = $grade['grade_id'];
        }
        return $grade_id;
    }
    /**
     * 新增等级
     */
    public function add($data)
    {
        $data['app_id'] = self::$app_id;
        return $this->save($data);
    }
    /**
     * 编辑等级
     */
    public function edit($data)
    {
        return $this->save($data);
    }
    /**
     * 删除等级
     */
    public function setDelete()
    {
        // 检查是否有用户使用此等级
        if (class_exists('app\common\model\plus\business\Business')) {
            $businessModel = new Business();
            $count = $businessModel->where('grade_id', '=', $this['grade_id'])->count();
            if ($count > 0) {
                return false;
            }
        }
        return $this->delete();
    }
}
admin/app/common/model/plus/business/Industry.php
New file
@@ -0,0 +1,127 @@
<?php
namespace app\common\model\plus\business;
use think\facade\Cache;
use app\common\model\BaseModel;
/**
 * 行业模型
 */
class Industry extends BaseModel
{
    protected $pk = 'industry_id';
    protected $name = 'industry';
    /**
     * 行业详情
     */
    public static function detail($industry_id)
    {
        return (new static())->find($industry_id);
    }
    /**
     * 所有行业
     */
    public static function getALL()
    {
        $model = new static;
        if (!Cache::get('industry_' . $model::$app_id)) {
            $data = $model->order(['sort' => 'asc', 'create_time' => 'asc'])->select();
            $all = !empty($data) ? $data->toArray() : [];
            $tree = [];
            foreach ($all as $first) {
                if ($first['parent_id'] != 0) continue;
                $twoTree = [];
                foreach ($all as $two) {
                    if ($two['parent_id'] != $first['industry_id']) continue;
                    $threeTree = [];
                    foreach ($all as $three)
                        $three['parent_id'] == $two['industry_id']
                        && $threeTree[$three['industry_id']] = $three;
                    !empty($threeTree) && $two['child'] = $threeTree;
                    array_push($twoTree, $two);
                }
                if (!empty($twoTree)) {
                    $temp_two_tree = array_column($twoTree, 'sort');
                    array_multisort($temp_two_tree, SORT_ASC, $twoTree);
                    $first['child'] = $twoTree;
                }
                array_push($tree, $first);
            }
            Cache::tag('cache')->set('industry_' . $model::$app_id, compact('all', 'tree'));
        }
        return Cache::get('industry_' . $model::$app_id);
    }
    /**
     * 获取所有行业
     */
    public static function getCacheAll()
    {
        return self::getALL()['all'];
    }
    /**
     * 获取所有行业(树状结构)
     */
    public static function getCacheTree()
    {
        return self::getALL()['tree'];
    }
    /**
     * 获取所有行业(树状结构)
     * @return string
     */
    public static function getCacheTreeJson()
    {
        return json_encode(static::getCacheTree());
    }
    /**
     * 获取指定行业下的所有子行业id
     */
    public static function getSubIndustryId($parent_id, $all = [])
    {
        $arrIds = [$parent_id];
        empty($all) && $all = self::getCacheAll();
        foreach ($all as $key => $item) {
            if ($item['parent_id'] == $parent_id) {
                unset($all[$key]);
                $subIds = self::getSubIndustryId($item['industry_id'], $all);
                !empty($subIds) && $arrIds = array_merge($arrIds, $subIds);
            }
        }
        return $arrIds;
    }
    /**
     * 指定的行业下是否存在子行业
     */
    protected static function hasSubIndustry($parentId)
    {
        $all = self::getCacheAll();
        foreach ($all as $item) {
            if ($item['parent_id'] == $parentId) {
                return true;
            }
        }
        return false;
    }
    /**
     * 获取所有一级行业
     */
    public static function getFirstIndustry()
    {
        return (new static())->where('parent_id', '=', 0)
            ->order(['sort' => 'asc', 'create_time' => 'asc'])
            ->select();
    }
    public function getListByIds($ids)
    {
        return $this->field(['industry_id', 'name', 'parent_id'])->where('industry_id', 'in', $ids)->select();
    }
}
admin/app/common/model/plus/business/Saving.php
New file
@@ -0,0 +1,52 @@
<?php
namespace app\common\model\plus\business;
use app\common\model\BaseModel;
/**
 * 名片保存(浏览)记录
 */
class Saving extends BaseModel
{
    protected $name='business_card_saving';
    public function user()
    {
        $model=self::getCalledModule()?:'common';
        return $this->hasOne("\\app\\$model\\model\\user\\User",'user_id','user_id');
    }
    public function affiliation()
    {
        $model=self::getCalledModule()?:'common';
        return $this->hasOne("\\app\\$model\\model\\user\\User",'user_id','affiliation_id');
    }
    public function business()
    {
        $model=self::getCalledModule()?:'common';
        return $this->hasOne("\\app\\$model\\model\\plus\\business\\Business",'business_card_id','business_card_id');
    }
    public function lists($param=[]){
        $paramL=array_merge(['listRows'=>15],$param);
        $where=[];
        !empty($paramL['user_id'])&&$where['user_id']=$paramL['user_id'];
        !empty($paramL['affiliation_id'])&&$where['affiliation_id']=$paramL['affiliation_id'];
        !empty($paramL['type'])&&$where['type']=$paramL['type'];
        return $this->with(['user'=>['businessCard'=>['image','logoImage']],'affiliation','business'=>['image']])->order('create_time','desc')->where($where)->paginate($paramL);
    }
    /**
     * 获取记录数量
     * @param $param
     * @return int|string
     * @throws \think\Exception
     */
    public function getQuantity($paramL=[])
    {
        $where=[];
        !empty($paramL['user_id'])&&$where['user_id']=$paramL['user_id'];
        !empty($paramL['affiliation_id'])&&$where['affiliation_id']=$paramL['affiliation_id'];
        !empty($paramL['today'])&&$this->whereTime('create_time', 'today');
        !empty($paramL['type'])&&$where['type']=$paramL['type'];
        return $this->where($where)->count();
    }
}
admin/app/common/model/plus/business/Template.php
New file
@@ -0,0 +1,36 @@
<?php
namespace app\common\model\plus\business;
use app\common\model\BaseModel;
/**
 * 名片模板
 */
class Template extends BaseModel
{
    protected $name='business_card_template';
    protected $pk='template_id';
    public function getList($param=[]){
        $paramL=array_merge(['listRows'=>15],$param);
        $where=[];
        return $this->order('create_time','desc')->where($where)->paginate($paramL);
    }
    public function add($data){
        $data['app_id']=self::$app_id;
        return $this->save($data);
    }
    /**
     * 获取模板详情
     * @param $template_id
     * @return array|false|\PDOStatement|string|Template
     * @throws \think\db\exception\DataNotFoundException
     * @throws \think\db\exception\ModelNotFoundException
     * @throws \think\exception\DbException
     */
    public function detail($template_id)
    {
        return $this->where(['template_id'=>$template_id])->find();
    }
}
admin/app/common/service/business/Poster.php
New file
@@ -0,0 +1,431 @@
<?php
namespace app\common\service\business;
use app\common\model\plus\business\Template;
use app\common\service\qrcode\Base;
use Grafika\Color;
use Grafika\Grafika;
/**
 * 生成名片
 * Class Qrcode
 * @package app\common\service
 */
class Poster extends Base
{
    /* @var array $dealer 用户信息 */
    private $dealer;
    /* @var array $config 名片设置 */
    private $config;
    public $template;
    /**
     * 构造方法
     * Poster constructor.
     * @param $dealer
     * @throws \Exception
     */
    public function __construct($dealer)
    {
        parent::__construct();
        // 用户信息
        $this->dealer = $dealer;
        $this->template = (new Template())->detail($dealer['template_id']);
        // 名片设置
        $this->config = json_decode($this->template['style'], true);
    }
    /**
     * 获取用户名片
     * @return string
     * @throws \app\common\exception\BaseException
     * @throws \think\exception\DbException
     * @throws \Exception
     */
    public function getImage($isType = true)
    {
        if (file_exists($this->getPosterPath('business')) && $isType) {
            return $this->getPosterUrl('business');
        }
        // 小程序id
        $appId = $this->dealer['app_id'];
        // 1. 下载背景图
        $backdrop = $this->saveTempImage($appId, $this->config['backdrop']['src'], 'backdrop');
        if ($this->dealer['file_path']) {
            // 2. 下载用户头像
            $avatarUrl = $this->saveTempImage($appId, $this->dealer['file_path'], 'avatar');
        } else {
            // 2. 下载用户头像
            $avatarUrl = $this->saveTempImage($appId, $this->dealer['user']['avatarUrl'], 'avatar');
        }
        $logo = '';
        if ($this->dealer['logoImage']['file_path']) {
            // 2. 下载logo
            $logo = $this->saveTempImage($appId, $this->dealer['logoImage']['file_path'], 'logo');
        }
        // 4. 拼接名片
        return $this->savePoster($backdrop, $avatarUrl, 'business', $logo);
    }
    /**
     * 名片图文件路径
     * @return string
     */
    private function getPosterPath($name)
    {
        // 保存路径
        $tempPath = $_SERVER['DOCUMENT_ROOT']. '/temp/'.$this->template['app_id'] . '/'.$name.'/';
        !is_dir($tempPath) && mkdir($tempPath, 0755, true);
        return $tempPath . $this->getPosterName();
    }
    /**
     * 名片文件名称
     * @return string
     */
    private function getPosterName()
    {
        return md5('poster_' . $this->dealer['business_card_id']) . '.png';
    }
    /**
     * 名片url
     * @return string
     */
    private function getPosterUrl($name)
    {
        return base_url() . '/temp/' . $this->template['app_id'] . '/' .$name . '/' . $this->getPosterName() . '?t=' . time();
    }
    /**
     * 拼接名片
     * @param $backdrop
     * @param $avatarUrl
     * @param $imageUrl
     * @param $logo
     * @return string
     * @throws \Exception
     */
    private function savePoster($backdrop, $avatarUrl, $imageUrl, $logo)
    {
        // 创建画布
        list($width, $height) = getimagesize($backdrop);
        $newImage = imagecreatetruecolor($width, $height);
        $backdropIm = $this->imagEcr($backdrop);
        imageantialias($newImage, true);
        // 将第一张图片覆盖在新图像上
        imagecopy($newImage, $backdropIm, 0, 0, 0, 0, $width, $height);
        if (!empty($this->config['icon'])) {
            foreach ($this->config['icon'] as $key => $value) {
                $this->addImagecopy($newImage, $value);
            }
        }
        // 写入地址
        //$this->addText($newImage, 'address', '地址:', 0, false, $width);
        if ($this->config['avatar']['display'] == 1) {
            // 生成圆形用户头像
            $this->config['avatar']['style'] === 'circle' && $this->circular($avatarUrl, $avatarUrl);
            // 重设用户头像宽高
            $avatarWidth = $this->config['avatar']['width'];
            //重设图片大小
            $this->resizeExact($avatarUrl,$avatarWidth,$avatarWidth);
            // 用户头像添加到背景图
            $avatarX = $this->config['avatar']['left'];
            $avatarY = $this->config['avatar']['top'];
            // 打开用户头像
            $avatarIm = $this->imagEcr($avatarUrl);
            imagecopy($newImage, $avatarIm, $avatarX, $avatarY, 0, 0, $avatarWidth, $avatarWidth);
        }
        if (is_file($logo) && $this->config['logo']['display'] == 1) {
            // 生成圆形logo
            $this->config['logo']['style'] === 'circle' && $this->circular($logo, $logo);
            //调整后大小
            $logoWidth = round($this->config['logo']['width']);
            $logoHeight = round($this->config['logo']['height']);
            //重设图片大小
            $this->resizeExact($logo,$logoWidth,$logoHeight);
            // 用户logo添加到背景图
            $logoX = $this->config['logo']['left'];
            $logoY = $this->config['logo']['top'];
            // 打开用户logo
            $logoImage = $this->imagEcr($logo);
            imagecopy($newImage, $logoImage, $logoX, $logoY, 0, 0, $logoWidth, $logoHeight);
        }
        // 写入用户昵称
        $this->addText($newImage, 'name');
        foreach ($this->dealer['unit'] as $key => $value) {
            // 写入公司
            $this->addText($newImage, 'unit', '', $key, true);
        }
        foreach ($this->dealer['address'] as $key => $value) {
            // 写入地址
            $this->addText($newImage, 'address', '', $key, true);
        }
        foreach ($this->dealer['duties'] as $key => $value) {
            // 写入职务
            $this->addText($newImage, 'duties', '', $key, true);
        }
        // 写入职位
        //$this->addText($newImage, 'duties');
        if (!empty($this->dealer['position']) && !empty($this->config['position'])) {
            foreach ($this->dealer['position'] as $key => $value) {
                // 写入公司
                $this->addText($newImage, 'position', '', $key, true);
            }
        }
        // 写入手机号
        $this->addText($newImage, 'mobile', '手机:');
        // 写入微信
        if ($this->dealer['mailbox']) {
            // 写入邮箱
            $this->addText($newImage, 'mailbox', '邮箱:');
        }
        if ($this->dealer['phone']) {
            $this->addText($newImage, 'phone', '电话:');
        }
        // 保存图片
        imagejpeg($newImage, $this->getPosterPath($imageUrl),100); // 根据需要选择合适的函数(如imagepng、imagegif等)
        // 清理内存
        imagedestroy($newImage);
        return $this->getPosterUrl($imageUrl);
    }
    /**
     * 打开图片
     * @param $image
     * @return false|resource
     */
    public function imagEcr($image)
    {
        // 创建画布
        return imagecreatefromstring(file_get_contents($image));
    }
    /**
     * 重设图片大小
     * @param $image
     * @param $newWidth
     * @param $newHeight
     * @return void
     */
    public function resizeExact($imageUrl, $newWidth, $newHeight )
    {
        $image=imagecreatefromstring(file_get_contents($imageUrl));
        // 调整图片大小
        $targetImage = imagecreatetruecolor($newWidth, $newHeight);
        imageantialias($targetImage, true);
        //获取图片大小
        $width = imagesx($image);
        $height = imagesy($image);
        // 这一句一定要有
        imagesavealpha($targetImage, true);
        $bg = imagecolorallocatealpha($targetImage, 255, 255, 255, 127);
        imagefill($targetImage, 0, 0, $bg);
        imagecopyresampled($targetImage, $image, 0, 0, 0, 0, $newWidth, $newHeight, $width, $height);
        imagepng($targetImage, $imageUrl,0); // 根据需要选择合适的函数(如imagepng、imagegif等)
        // 清理内存
        imagedestroy($targetImage);
    }
    /**
     * 添加图标
     * @param $newImage
     * @param $data
     * @return void
     */
    public function addImagecopy($newImage,$data){
        $src=$this->saveTempImage($this->template['app_id'], $data['src'], 'icon');
        $this->resizeExact($src,$data['width'], $data['height']);
        $backdropIm = $this->imagEcr($src);
        // 将第一张图片覆盖在新图像上
        imagecopy($newImage, $backdropIm, $data['left'], $data['top'], 0, 0, $data['width'], $data['height']);
    }
    /**
     * 写入文字
     * @param $editor
     * @param $backdropImage
     * @param $name
     * @param $text
     * @param $key
     * @param $type
     * @param $width
     * @return array|bool
     */
    public function addText($editor, $name, $text = '', $key = 0, $type = false, $width = 0)
    {
        $fontPath = $_SERVER['DOCUMENT_ROOT']. '/fonts/MSYH.TTC';
        if ($type) {
            list($fontSize, $fontX,$fontY)  = self::SizeLeftTop($this->config[$name][$key]['fontSize'], $this->config[$name][$key]['left'],$this->config[$name][$key]['top']);
            $colorResource=self::colorResource($this->config[$name][$key]['color'],$editor);
            return imagettftext($editor, $fontSize, 0, $fontX, $fontY, $colorResource, $fontPath, $text . $this->dealer[$name][$key]);
        } else if ($name == 'mobile') {
            list($fontSize, $fontX,$fontY)  = self::SizeLeftTop($this->config[$name]['fontSize'], $this->config[$name]['left'],$this->config[$name]['top']);
            $text = $text . $this->dealer['mobile'];
            if (!empty($this->dealer['mobile_phone'])) {
                $text = $text . ' / ' . $this->dealer['mobile_phone'];
            }
            $colorResource=self::colorResource($this->config[$name]['color'],$editor);
            return imagettftext($editor, $fontSize, 0, $fontX, $fontY, $colorResource, $fontPath, $text);
        }  /*else if ($name == 'duties') {
            list($fontSize, $fontX,$fontY)  = self::SizeLeftTop($this->config[$name][$key]['fontSize'], $this->config[$name][$key]['left'],$this->config[$name][$key]['top']);
            $duties = $this->dealer['duties'][$key];
           if (!empty($this->dealer['duties'][1])) {
                $duties = $duties . ' / ' . $this->dealer['duties'][1];
            }
            if (!empty($this->dealer['duties'][2])) {
                $duties = $duties . ' / ' . $this->dealer['duties'][2];
            }
            $colorResource=self::colorResource($this->config['duties'][$key]['color'],$editor);
            return imagettftext($editor, $fontSize, 0, $fontX, $fontY, $colorResource, $fontPath, $text . $duties);
        }*/ else if ($name == 'address') {
            list($fontSize, $fontX,$fontY)  = self::SizeLeftTop($this->config[$name][$key]['fontSize'], $this->config[$name][$key]['left'],$this->config[$name][$key]['top']);
            $title = $this->dealer['address'][$key];
            $strlen = mb_strlen($title, 'utf-8');
            $left = $width - $this->config['address'][$key]['left'] - $fontSize;
            $titleNum = bcdiv($left, $this->config['address'][$key]['fontSize']);
            $the_box = $this->config['address'][$key]['fontSize'] * 3;
            while ($width < ($titleNum * $this->config['address'][$key]['fontSize'] + $the_box + $fontX)) {
                $titleNum--;
            };
            if ($strlen > $titleNum && $titleNum) {
                $strArr = self::mbStrSplit($title, $titleNum);
            }
            $colorResource=self::colorResource($this->config['address'][$key]['color'],$editor);
            if ($strlen > $titleNum && $titleNum) {
                $y = $fontY + 10;
                foreach ($strArr as $k => $v) {
                    if ($k == 0) {
                        imagettftext($editor, $fontSize, 0, $fontX, $fontY, $colorResource, $fontPath, $text . $v);
                    } else {
                        $y = $y + ($fontSize * $k);
                        imagettftext($editor, $fontSize, 0, $fontX + $the_box, $y, $colorResource, $fontPath, $v);
                    }
                }
                return true;
            } else {
                return imagettftext($editor, $fontSize, 0, $fontX, $fontY, $colorResource, $fontPath, $text . $title);
            }
        } else {
            list($fontSize, $fontX,$fontY)  = self::SizeLeftTop($this->config[$name]['fontSize'], $this->config[$name]['left'],$this->config[$name]['top']);
            $colorResource=self::colorResource($this->config[$name]['color'],$editor);
            return imagettftext($editor, $fontSize, 0, $fontX, $fontY, $colorResource, $fontPath, $text . $this->dealer[$name]); // path/to/font.ttf为自定义字体路径
        }
    }
    /**
     * 分割字符串为数组模式
     * @param $string
     * @param $len
     * @return mixed
     */
    private function mbStrSplit($string, $len = 1)
    {
        $start = 0;
        $strlen = mb_strlen($string);
        while ($strlen) {
            $array[] = mb_substr($string, $start, $len, "utf8");
            $string = mb_substr($string, $len, $strlen, "utf8");
            $strlen = mb_strlen($string);
        }
        return $array;
    }
    /**
     * 生成圆形图片
     * @param static $imgpath 图片地址
     * @param string $saveName 保存文件名,默认空。
     */
    private function circular($imgpath, $saveName = '')
    {
        $srcImg = imagecreatefromstring(file_get_contents($imgpath));
        $w = imagesx($srcImg);
        $h = imagesy($srcImg);
        $w = $h = min($w, $h);
        $newImg = imagecreatetruecolor($w, $h);
        // 这一句一定要有
        imagesavealpha($newImg, true);
        // 拾取一个完全透明的颜色,最后一个参数127为全透明
        $bg = imagecolorallocatealpha($newImg, 255, 255, 255, 127);
        imagefill($newImg, 0, 0, $bg);
        $r = $w / 2; //圆半径
        for ($x = 0; $x < $w; $x++) {
            for ($y = 0; $y < $h; $y++) {
                $rgbColor = imagecolorat($srcImg, $x, $y);
                if (((($x - $r) * ($x - $r) + ($y - $r) * ($y - $r)) < ($r * $r))) {
                    imagesetpixel($newImg, $x, $y, $rgbColor);
                }
            }
        }
        // 输出图片到文件
        imagepng($newImg, $saveName,0);
        // 释放空间
        imagedestroy($srcImg);
        imagedestroy($newImg);
    }
    /**
     * 获取名片模板
     * @return string
     * @throws \app\common\exception\BaseException
     * @throws \think\exception\DbException
     * @throws \Exception
     */
    public function getImageE($config, $template_id = '')
    {
        $model = new Template();
        $this->config = $config;
        // 小程序id
        $appId = $this->template['app_id'];
        // 1. 下载背景图
        $backdrop = $this->saveTempImage($appId, $this->config['backdrop']['src'], 'backdrop');
        // 2. 下载用户头像
        $avatarUrl = $this->saveTempImage($appId, $model::$base_url."/image/agent/avatar.jpg", 'avatar', $template_id);
        // 2. 下载logo
        $logo = $this->saveTempImage($appId, $this->config['logo']['src'], 'logo');
        // 4. 拼接名片
        return $this->savePoster($backdrop, $avatarUrl, 'template', $logo);
    }
    /**
     * 获取颜色
     * @param $color
     * @param $editor
     * @return false|int
     */
    private function colorResource($color, $editor)
    {
        $Color = new Color($color);
        list($r, $g, $b, $alpha) = $Color->getRgba();
        $scale = round(127 * $alpha);
        $invert = 127 - $scale;
        return imagecolorallocatealpha(
            $editor,
            $r, $g, $b,
            $invert
        );
    }
    /**
     *计算字体大小和顶部跟左边的距离
     * @param $fontSize
     * @param $left
     * @param $top
     * @return array
     */
    private function SizeLeftTop($fontSize,$left,$top){
        $data[0] = $fontSize * 0.76;
        $data[1]  = $left;
        $data[2]  = $top + $fontSize;
        return $data;
    }
}
admin/app/shop/controller/plus/business/Business.php
New file
@@ -0,0 +1,30 @@
<?php
namespace app\shop\controller\plus\business;
use app\shop\controller\Controller;
use app\shop\model\plus\business\Business as BusinessModel;
/**
 * 名片管理
 */
class Business extends Controller
{
    public function  index(){
        $list=(new BusinessModel)->getList(request()->request());
        return  $this->renderSuccess('',compact('list'));
    }
    public function edit(){
        $param=request()->param();$param=$param['business'];
        $model=(new BusinessModel())->get($param['business_card_id']);
        if($model->add($param)){
            return $this->renderSuccess('编辑成功');
        }
        return $this->renderError('编辑失败');
    }
    public function delete(){
        $param=request()->param();
        if((new BusinessModel())->where('business_card_id',$param['business_card_id'])->delete()){
            return $this->renderSuccess('删除成功');
        }
        return $this->renderError('删除失败');
    }
}
admin/app/shop/controller/plus/business/Grade.php
New file
@@ -0,0 +1,79 @@
<?php
namespace app\shop\controller\plus\business;
use app\shop\model\plus\business\Grade as GradeModel;
use app\shop\controller\Controller;
/**
 * 名片等级控制器
 */
class Grade extends Controller
{
    /**
     * 名片等级列表
     */
    public function index()
    {
        $model = new GradeModel;
        $list = $model->getList($this->postData());
        return $this->renderSuccess('', compact('list'));
    }
    /**
     * 添加等级
     */
    public function add()
    {
        $model = new GradeModel;
        // 新增记录
        if ($model->add($this->postData())) {
            return $this->renderSuccess('添加成功');
        }
        return $this->renderError($model->getError() ?: '添加失败');
    }
    /**
     * 编辑等级
     */
    public function edit($grade_id)
    {
        $model = GradeModel::detail($grade_id);
        if (!$model) {
            return $this->renderError('等级不存在');
        }
        // 修改记录
        if ($model->edit($this->postData())) {
            return $this->renderSuccess('修改成功');
        }
        return $this->renderError($model->getError() ?: '修改失败');
    }
    /**
     * 删除等级
     */
    public function delete($grade_id)
    {
        // 等级详情
        $model = GradeModel::detail($grade_id);
        if (!$model) {
            return $this->renderError('等级不存在');
        }
        if (!$model->setDelete()) {
            return $this->renderError('已存在使用此等级的名片,删除失败');
        }
        return $this->renderSuccess('删除成功');
    }
    /**
     * 获取等级详情
     */
    public function detail($grade_id)
    {
        $model = GradeModel::detail($grade_id);
        if (!$model) {
            return $this->renderError('等级不存在');
        }
        return $this->renderSuccess('', compact('model'));
    }
}
admin/app/shop/controller/plus/business/Industry.php
New file
@@ -0,0 +1,115 @@
<?php
namespace app\shop\controller\plus\business;
use app\shop\controller\Controller;
use app\shop\model\plus\business\Industry as IndustryModel;
use think\facade\Cache;
class Industry extends Controller
{
    /**
     * 行业列表
     */
    public function index()
    {
        $model = new IndustryModel;
        $list = $model->getALL();
        return $this->renderSuccess('',compact('list'));
    }
    /**
     * 行业详情
     */
    public function detail($industry_id)
    {
        $industry = IndustryModel::detail($industry_id);
        if (!$industry) {
            return $this->renderError('行业不存在');
        }
        return $this->renderSuccess(compact('industry'));
    }
    /**
     * 添加行业
     */
    public function add()
    {
        $model = new IndustryModel;
        // 获取post数据
        $data = $this->request->post();
        // 添加行业
        if ($model->add($data)) {
            // 清理缓存
            Cache::tag('cache')->clear();
            return $this->renderSuccess('添加成功');
        }
        return $this->renderError($model->getError() ?: '添加失败');
    }
    /**
     * 编辑行业
     */
    public function edit($industry_id)
    {
        // 行业详情
        $industry = IndustryModel::detail($industry_id);
        if (!$industry) {
            return $this->renderError('行业不存在');
        }
        // 验证表单
        if (!$this->request->isPost()) {
            return $this->renderError('请求方式错误');
        }
        // 获取post数据
        $data = $this->request->post();
        // 编辑行业
        if ($industry->edit($data)) {
            // 清理缓存
            Cache::tag('cache')->clear();
            return $this->renderSuccess('编辑成功');
        }
        return $this->renderError($industry->getError() ?: '编辑失败');
    }
    /**
     * 删除行业
     */
    public function delete($industry_id)
    {
        // 行业详情
        $industry = IndustryModel::detail($industry_id);
        if (!$industry) {
            return $this->renderError('行业不存在');
        }
        // 检查是否有子行业
        if (IndustryModel::hasSubIndustry($industry_id)) {
            return $this->renderError('该行业下存在子行业,无法删除');
        }
        // 删除行业
        if ($industry->delete()) {
            // 清理缓存
            Cache::tag('cache')->clear();
            return $this->renderSuccess('删除成功');
        }
        return $this->renderError('删除失败');
    }
    /**
     * 获取一级行业列表
     */
    public function getFirstIndustry()
    {
        $list = IndustryModel::getFirstIndustry();
        return $this->renderSuccess(compact('list'));
    }
    /**
     * 根据上级ID获取子行业
     */
    public function getSubIndustry($parent_id = 0)
    {
        $model = new IndustryModel;
        $list = $model->where('parent_id', '=', $parent_id)->order(['sort' => 'asc', 'create_time' => 'asc'])->select();
        return $this->renderSuccess(compact('list'));
    }
}
admin/app/shop/controller/plus/business/Template.php
New file
@@ -0,0 +1,195 @@
<?php
namespace app\shop\controller\plus\business;
use app\common\service\business\Poster;
use app\shop\model\plus\business\Template as BusinessTemplate;
use app\shop\controller\Controller;
class Template extends Controller
{
    public function index()
    {
        $list = (new BusinessTemplate())->getList();
        return $this->renderSuccess('', compact('list'));
    }
    public function edit()
    {
        $template_id = input('template_id');
        $model = (new BusinessTemplate())->where('template_id', $template_id)->find();
        if (request()->isGet()) {
            $data = json_decode($model['style'], true);
            empty($data['position']) ? $data['position'] = [] : '';
            empty($data['is_business']) ? $data['is_business'] = 0 : '';
            empty($data['positionNum']) ? $data['positionNum'] = 0 : '';
            empty($data['icon']) ? $data['icon'] = [] : '';
            $data = json_encode($data, JSON_UNESCAPED_UNICODE);
            return $this->renderSuccess('', compact('data'));
        }
        $dealer = ['business_card_id' => $template_id, 'name' => 'XXX', 'unit' => [], 'duties' => [], 'address' => [], 'mobile' => 'xxxxxxxxxxx', 'wechat' => 'xxxxxxxxxxx', 'mailbox' => 'xxxxxxxxxxx@.xxx.com', 'phone' => 'xxx-xxx-xxx', 'website' => 'xxxxxxxxxxxxxx.com', 'fax' => 'xxx-xxx', 'zip_code' => 'xxxxxx', 'template_id' => $template_id, 'wxapp_id' => 10001];
        $param = request()->param();
        $imageInfo = getimagesize($param['template']['backdrop']['src']);
        $param['template']['backdrop']['height'] = $imageInfo[1];
        $param['template']['backdrop']['width'] = $imageInfo[0];
        foreach ($param['template']['unit'] as $key => $value) {
            // 写入公司
            $dealer['unit'][] = 'xxxxx公司' . ($key + 1);
        }
        // 写入职位
        //$dealer['duties'][] = '职位';
        foreach ($param['template']['duties'] as $key => $value) {
            // 写入职位
            $dealer['duties'][] = '职位'.($key + 1);
        }
        foreach ($param['template']['address'] as $key => $value) {
           // 写入地址
            $dealer['address'][] = '广西壮族自治区南宁市江南区壮锦大道八桂绿城·龙湖御景-A栋-2单元'.($key + 1).'号';
        }
        $Qrcode = new Poster($dealer);
        $paramL['image'] = $Qrcode->getImageE($param['template'], $template_id);
        $paramL['style'] = json_encode($param['template'], JSON_UNESCAPED_UNICODE);
        if ($model->add($paramL)) {
            return $this->renderSuccess('编辑成功', url('business.template/index'));
        }
        return $this->renderError('编辑失败');
    }
    public function add()
    {
        $model = new BusinessTemplate();
        if (request()->isGet()) {
            $data = ["backdrop" => [
                "src" => $model::$base_url."/image/agent/agent-bg.jpg",
                'type' => 'backdrop'
            ],
                "is_business" => 0,
                "name" => [
                    "fontSize" => 14,
                    "color" => "#000000",
                    "left" => 232,
                    "top" => 13,
                    "fontWeight" => 400,
                    'type' => 'text'
                ],
                "avatar" => [
                    "width" => 70,
                    "style" => "circle",
                    "left" => 37,
                    "top" => 37,
                    "display" => 1,
                    "src" => $model::$base_url."/image/agent/avatar.jpg",
                    'type' => 'avatar'
                ],
                "logo" => [
                    "width" => 70,
                    "height" => 70,
                    "style" => "square",
                    "left" => 22,
                    "src" => $model::$base_url."/image/diy/logo_top.png",
                    "top" => 140,
                    "display" => 1,
                    'type' => 'image'
                ],
                "mobile" => [
                    "fontSize" => 14,
                    "color" => "#000000",
                    "left" => 192,
                    "top" => 43,
                    "fontWeight" => 400
                ],
                "address" => [
                    ["fontSize" => 14,
                        "color" => "#000000",
                        "left" => 133,
                        "top" => 206,
                        "fontWeight" => 400,
                        'type' => 'text']
                ],
                "unit" => [
                    ["fontSize" => 14,
                        "color" => "#000000",
                        "left" => 133,
                        "top" => 167,
                        "fontWeight" => 100,
                        'type' => 'text']
                ],
                "duties" => [
                    ["fontSize" => 14,
                        "color" => "#000000",
                        "left" => 260,
                        "top" => 167,
                        "fontWeight" => 400,
                        'type' => 'text']
                ],
                "position" => [
                ],
                "wechat" => [
                    "fontSize" => 14,
                    "color" => "#000000",
                    "left" => 205,
                    "top" => 65,
                    "fontWeight" => 400,
                    'type' => 'text'
                ],
                "mailbox" => [
                    "fontSize" => 14,
                    "color" => "#000000",
                    "left" => 205,
                    "top" => 104,
                    "fontWeight" => 400,
                    'type' => 'text'
                ],
                "phone" => [
                    "fontSize" => 14,
                    "color" => "#000000",
                    "left" => 205,
                    "top" => 84,
                    "fontWeight" => 400,
                    'type' => 'text'
                ],
                'positionNum' => 0,
                "iconL" => [],
            ];
            return $this->renderSuccess('', [
                'data' => json_encode($data, JSON_UNESCAPED_UNICODE)
            ]);
        }
        $param = request()->param();
        $imageInfo = getimagesize($param['template']['backdrop']['src']);
        $param['template']['backdrop']['height'] = $imageInfo[1];
        $param['template']['backdrop']['width'] = $imageInfo[0];
        $paramL['style'] = json_encode($param['template'], JSON_UNESCAPED_UNICODE);
        if ($model->add($paramL)) {
            $template_id = $model->template_id;
            $dealer = ['business_card_id' => $template_id, 'name' => 'XXX', 'unit' => [], 'duties' => [], 'address' => [], 'mobile' => 'xxxxxxxxxxx', 'wechat' => 'xxxxxxxxxxx', 'mailbox' => 'Xxxxxxxxxxxxxxxxx', 'phone' => 'xxx-xxx-xxx', 'website' => 'xxxxxxxxxxxxxx.com', 'fax' => 'xxx-xxx', 'zip_code' => 'xxxxxx', 'template_id' => $template_id, 'wxapp_id' => 10001];
            foreach ($param['template']['unit'] as $key => $value) {
                // 写入公司
                $dealer['unit'][] = 'xxxxx公司' . ($key + 1);
            }
            // 写入职位
            $dealer['duties'][] = '职位';
            // 写入地址
            $dealer['address'][] = '地址1号';
            $Qrcode = new Poster($dealer);
            $paramI['image'] = $Qrcode->getImageE($param['template'], $template_id);
            $modelBusiness = (new BusinessTemplate())->where(['template_id' => $template_id])->find();
            $modelBusiness->where(['template_id' => $template_id])->update($paramI);
            return $this->renderSuccess('添加成功', url('business.template/index'));
        }
        return $this->renderError('添加失败');
    }
    public function delete($template_id)
    {
        if ((new BusinessTemplate())->where(['template_id' => $template_id])->delete()) {
            return $this->renderSuccess('删除成功');
        }
        return $this->renderError('删除失败');
    }
}
admin/app/shop/model/plus/business/Business.php
New file
@@ -0,0 +1,12 @@
<?php
namespace app\shop\model\plus\business;
use app\common\model\plus\business\Business as BusinessModel;
/**
 * 名片管理模型
 */
class Business extends BusinessModel
{
}
admin/app/shop/model/plus/business/Grade.php
New file
@@ -0,0 +1,96 @@
<?php
namespace app\shop\model\plus\business;
use app\common\model\plus\business\Grade as GradeModel;
/**
 * 名片等级模型
 */
class Grade extends GradeModel
{
    /**
     * 获取列表记录
     */
    public function getList($data = [])
    {
        $list = $this->selectList();
        // 如果为空,则插入默认等级
        if(count($list) == 0) {
            $this->save([
                'name' => '默认等级',
                'price' => 0.00,
                'weight' => 1,
                'app_id' => self::$app_id,
                'create_time' => time(),
                'update_time' => time()
            ]);
            $list = $this->selectList();
        }
        return $list;
    }
    /**
     * 新增等级
     */
    public function add($data)
    {
        // 验证数据
        if(empty($data['name'])) {
            $this->error = '等级名称不能为空';
            return false;
        }
        if(!isset($data['price']) || $data['price'] === '') {
            $data['price'] = 0.00;
        } else {
            $data['price'] = floatval($data['price']);
            if($data['price'] < 0) {
                $this->error = '查看联系方式价格不能小于0';
                return false;
            }
        }
        if(!isset($data['weight']) || $data['weight'] === '') {
            $data['weight'] = 100;
        } else {
            $data['weight'] = intval($data['weight']);
            if($data['weight'] < 0) {
                $this->error = '权重不能小于0';
                return false;
            }
        }
        return parent::add($data);
    }
    /**
     * 编辑等级
     */
    public function edit($data)
    {
        // 验证数据
        if(empty($data['name'])) {
            $this->error = '等级名称不能为空';
            return false;
        }
        if(!isset($data['price']) || $data['price'] === '') {
            $data['price'] = 0.00;
        } else {
            $data['price'] = floatval($data['price']);
            if($data['price'] < 0) {
                $this->error = '查看联系方式价格不能小于0';
                return false;
            }
        }
        if(!isset($data['weight']) || $data['weight'] === '') {
            $data['weight'] = 100;
        } else {
            $data['weight'] = intval($data['weight']);
            if($data['weight'] < 0) {
                $this->error = '权重不能小于0';
                return false;
            }
        }
        return parent::edit($data);
    }
}
admin/app/shop/model/plus/business/Industry.php
New file
@@ -0,0 +1,75 @@
<?php
namespace app\shop\model\plus\business;
use app\common\model\plus\business\Industry as CommonIndustry;
use think\facade\Cache;
class Industry extends CommonIndustry
{
    /**
     * 添加行业
     */
    public function add($data)
    {
        // 开启事务
        $this->startTrans();
        try {
            // 写入数据
            $this->save([
                'name' => $data['name'],
                'parent_id' => isset($data['parent_id']) ? $data['parent_id'] : 0,
                'sort' => isset($data['sort']) ? $data['sort'] : 0,
                'app_id' => self::$app_id,
                'create_time' => time(),
                'update_time' => time(),
                'status' => 1
            ]);
            // 提交事务
            $this->commit();
            // 清理缓存
            Cache::tag('cache')->clear();
            return true;
        } catch (\Exception $e) {
            // 回滚事务
            $this->rollback();
            $this->error = $e->getMessage();
            return false;
        }
    }
    /**
     * 编辑行业
     */
    public function edit($data)
    {
        // 检查是否将自己或子行业设为上级
        if (isset($data['parent_id']) && $data['parent_id'] > 0) {
            $subIds = $this->getSubIndustryId($this['industry_id']);
            if (in_array($data['parent_id'], $subIds)) {
                $this->error = '不能将自己或子行业设为上级';
                return false;
            }
        }
        // 开启事务
        $this->startTrans();
        try {
            // 更新数据
            $this->allowField(['name', 'parent_id', 'sort', 'update_time'])->save([
                'name' => $data['name'],
                'parent_id' => isset($data['parent_id']) ? $data['parent_id'] : 0,
                'sort' => isset($data['sort']) ? $data['sort'] : 0,
                'update_time' => time(),
            ]);
            // 提交事务
            $this->commit();
            // 清理缓存
            Cache::tag('cache')->clear();
            return true;
        } catch (\Exception $e) {
            // 回滚事务
            $this->rollback();
            $this->error = $e->getMessage();
            return false;
        }
    }
}
admin/app/shop/model/plus/business/Template.php
New file
@@ -0,0 +1,12 @@
<?php
namespace app\shop\model\plus\business;
use app\common\model\plus\business\Template as TemplateModel;
/**
 * 名片模板
 */
class Template extends TemplateModel
{
}
mobile/pages/plus/business/add.vue
New file
@@ -0,0 +1,495 @@
<template>
    <view>
        <header-bar title="编辑名片" :isBack="true" @click="back"></header-bar>
        <scroll-view scroll-y="true" class="scroll-view">
            <!-- 名片预览区域 -->
            <view class="preview-section">
                <view class="preview-title">名片预览</view>
                <view class="preview-card" :style="previewStyle">
                    <image v-if="business.background_image" class="preview-bg" :src="business.background_image" mode="aspectFill"></image>
                    <view class="preview-content">
                        <image v-if="file_path" class="preview-avatar" :src="file_path" mode="aspectFill"></image>
                        <view class="preview-info">
                            <view class="preview-name">{{business.real_name || '姓名'}}</view>
                            <view class="preview-company">{{business.company_name || '公司名称'}}</view>
                            <view class="preview-position">{{business.position || '职位'}}</view>
                        </view>
                    </view>
                </view>
            </view>
            <!-- 模板选择 -->
            <view class="template-section">
                <view class="section-title">选择模板</view>
                <scroll-view scroll-x="true" class="template-scroll">
                    <view class="template-item" v-for="(template, index) in templateList" :key="index"
                        :class="{active: template_id === template.template_id}" @click="selectTemplate(index)">
                        <image class="template-img" :src="template.preview_image" mode="aspectFill"></image>
                    </view>
                </scroll-view>
            </view>
            <!-- 表单内容 -->
            <form @submit="submitForm" class="form-section">
                <!-- 基本信息 -->
                <view class="form-group">
                    <view class="group-title">基本信息</view>
                    <!-- 头像上传 -->
                    <view class="form-item" v-if="avatar_display">
                        <view class="item-label">头像</view>
                        <view class="upload-area" @click="uploadImage('avatar')">
                            <image v-if="file_path" class="upload-img" :src="file_path" mode="aspectFill"></image>
                            <view v-else class="upload-placeholder">
                                <text class="icon iconfont icon-camera"></text>
                                <text>上传头像</text>
                            </view>
                        </view>
                    </view>
                    <!-- 姓名 -->
                    <view class="form-item">
                        <view class="item-label">姓名</view>
                        <input type="text" class="item-input" v-model="business.real_name" placeholder="请输入姓名" name="real_name" />
                    </view>
                    <!-- 公司名称 -->
                    <view class="form-item">
                        <view class="item-label">公司名称</view>
                        <input type="text" class="item-input" v-model="business.company_name" placeholder="请输入公司名称" name="company_name" />
                    </view>
                    <!-- 职位 -->
                    <view class="form-item">
                        <view class="item-label">职位</view>
                        <input type="text" class="item-input" v-model="business.position" placeholder="请输入职位" name="position" />
                    </view>
                    <!-- 公司Logo -->
                    <view class="form-item" v-if="logo_display">
                        <view class="item-label">公司Logo</view>
                        <view class="upload-area" @click="uploadImage('logo')">
                            <image v-if="logo_path" class="upload-img" :src="logo_path" mode="aspectFill"></image>
                            <view v-else class="upload-placeholder">
                                <text class="icon iconfont icon-camera"></text>
                                <text>上传Logo</text>
                            </view>
                        </view>
                    </view>
                </view>
                <!-- 联系方式 -->
                <view class="form-group">
                    <view class="group-title">联系方式</view>
                    <!-- 手机号 -->
                    <view class="form-item">
                        <view class="item-label">手机号</view>
                        <input type="number" class="item-input" v-model="business.phone" placeholder="请输入手机号" name="phone" />
                    </view>
                    <!-- 备用电话 -->
                    <view class="form-item">
                        <view class="item-label">备用电话</view>
                        <input type="number" class="item-input" v-model="business.mobile" placeholder="请输入备用电话" name="mobile" />
                    </view>
                    <!-- 邮箱 -->
                    <view class="form-item">
                        <view class="item-label">邮箱</view>
                        <input type="text" class="item-input" v-model="business.email" placeholder="请输入邮箱" name="email" />
                    </view>
                    <!-- 地址 -->
                    <view class="form-item">
                        <view class="item-label">地址</view>
                        <input type="text" class="item-input" v-model="business.address" placeholder="请输入地址" name="address" />
                    </view>
                </view>
                <!-- 详细信息 -->
                <view class="form-group">
                    <view class="group-title">详细信息</view>
                    <!-- 个人简介 -->
                    <view class="form-item">
                        <view class="item-label">个人简介</view>
                        <textarea class="item-textarea" v-model="business.intro" placeholder="请输入个人简介" name="intro" />
                    </view>
                    <!-- 业务范围 -->
                    <view class="form-item">
                        <view class="item-label">业务范围</view>
                        <textarea class="item-textarea" v-model="business.business_scope" placeholder="请输入业务范围" name="business_scope" />
                    </view>
                </view>
                <!-- 保存按钮 -->
                <view class="submit-section">
                    <button form-type="submit" class="submit-btn">保存</button>
                </view>
            </form>
        </scroll-view>
        <!-- 上传图片组件 -->
        <Upload v-if="isUpload" @getImgs="handleUpload" @close="closeUpload"></Upload>
    </view>
</template>
<script>
    import Upload from '@/components/upload/uploadOne.vue';
    export default {
        components: {
            Upload
        },
        data() {
            return {
                business: {
                    real_name: '',
                    company_name: '',
                    position: '',
                    phone: '',
                    mobile: '',
                    email: '',
                    address: '',
                    intro: '',
                    business_scope: '',
                    background_image: ''
                },
                templateList: [],
                template_id: '',
                file_id: '',
                file_path: '',
                logo_id: '',
                logo_path: '',
                business_card_id: '',
                avatar_display: true,
                logo_display: true,
                isUpload: false,
                uploadType: '',
                previewStyle: {}
            };
        },
        onLoad(options) {
            if (options.business_card_id) {
                this.business_card_id = options.business_card_id;
                this.getBusinessDetail();
            }
            this.getTemplateList();
        },
        methods: {
            back() {
                uni.navigateBack();
            },
            // 获取模板列表
            getTemplateList() {
                let _this = this;
                uni.getSystemInfo({ success: function(res) { _this.systemInfo = res; } });
                _this._post('plus.business/template/getList', { screenWidth: _this.systemInfo.screenWidth }, function(res) {
                    _this.templateList = res.data;
                    if (_this.templateList.length > 0 && !_this.template_id) {
                        _this.template_id = _this.templateList[0].template_id;
                        _this.selectTemplate(0);
                    }
                });
            },
            // 获取名片详情
            getBusinessDetail() {
                let _this = this;
                _this._post('plus.business/business/detail', { business_card_id: _this.business_card_id }, function(res) {
                    if (res.data) {
                        _this.business = res.data;
                        _this.template_id = res.data.template_id;
                        _this.file_id = res.data.file_id;
                        _this.file_path = res.data.file_path || '';
                        _this.logo_id = res.data.logo_id;
                        _this.logo_path = res.data.logo_path || '';
                        // 找到对应模板并应用样式
                        _this.templateList.forEach((template, index) => {
                            if (template.template_id === _this.template_id) {
                                _this.selectTemplate(index);
                            }
                        });
                    }
                });
            },
            // 选择模板
            selectTemplate(index) {
                const template = this.templateList[index];
                this.template_id = template.template_id;
                // 应用模板样式
                this.avatar_display = template.style?.avatar?.display !== false;
                this.logo_display = template.style?.logo?.display !== false;
                this.previewStyle = {
                    backgroundColor: template.style?.background?.color || '#37bde6',
                    color: template.style?.text?.color || '#fff'
                };
                // 如果有背景图优先级高于背景色
                if (template.background_image) {
                    this.business.background_image = template.background_image;
                }
            },
            // 上传图片
            uploadImage(type) {
                this.uploadType = type;
                this.isUpload = true;
            },
            // 处理上传结果
            handleUpload(data) {
                if (data && data.length > 0) {
                    const file = data[0];
                    if (this.uploadType === 'avatar') {
                        this.file_id = file.file_id;
                        this.file_path = file.file_path;
                    } else if (this.uploadType === 'logo') {
                        this.logo_id = file.file_id;
                        this.logo_path = file.file_path;
                    }
                }
                this.closeUpload();
            },
            // 关闭上传组件
            closeUpload() {
                this.isUpload = false;
                this.uploadType = '';
            },
            // 表单提交
            submitForm(e) {
                const formData = e.detail.value;
                // 表单验证
                if (!formData.real_name) {
                    this.showError('请输入姓名');
                    return false;
                }
                if (!formData.phone) {
                    this.showError('请输入手机号');
                    return false;
                }
                // 组装提交数据
                const submitData = {
                    ...formData,
                    template_id: this.template_id,
                    file_id: this.file_id,
                    logo_id: this.logo_id
                };
                // 判断是新增还是编辑
                let url = 'plus.business/business/add';
                if (this.business_card_id) {
                    url = 'plus.business/business/edit';
                    submitData.business_card_id = this.business_card_id;
                }
                // 提交表单
                let _this = this;
                _this._post(url, submitData, function(res) {
                    _this.showSuccess(res.msg, function() {
                        uni.navigateBack();
                    });
                });
            }
        }
    };
</script>
<style lang="scss">
    .scroll-view {
        height: calc(100vh - 80rpx);
    }
    .preview-section {
        background: #fff;
        padding: 30rpx;
        margin-bottom: 20rpx;
        .preview-title {
            font-size: 32rpx;
            font-weight: bold;
            color: #333;
            margin-bottom: 20rpx;
        }
        .preview-card {
            height: 400rpx;
            border-radius: 20rpx;
            overflow: hidden;
            position: relative;
            .preview-bg {
                position: absolute;
                top: 0;
                left: 0;
                width: 100%;
                height: 100%;
                z-index: 1;
            }
            .preview-content {
                position: relative;
                z-index: 2;
                display: flex;
                align-items: center;
                padding: 40rpx;
                color: #fff;
                .preview-avatar {
                    width: 150rpx;
                    height: 150rpx;
                    border-radius: 50%;
                    border: 4rpx solid #fff;
                }
                .preview-info {
                    margin-left: 30rpx;
                    .preview-name {
                        font-size: 44rpx;
                        font-weight: bold;
                        margin-bottom: 10rpx;
                    }
                    .preview-company {
                        font-size: 32rpx;
                        margin-bottom: 8rpx;
                        opacity: 0.9;
                    }
                    .preview-position {
                        font-size: 28rpx;
                        opacity: 0.8;
                    }
                }
            }
        }
    }
    .template-section {
        background: #fff;
        padding: 30rpx;
        margin-bottom: 20rpx;
        .section-title {
            font-size: 32rpx;
            font-weight: bold;
            color: #333;
            margin-bottom: 20rpx;
        }
        .template-scroll {
            white-space: nowrap;
            padding-bottom: 10rpx;
            .template-item {
                display: inline-block;
                width: 200rpx;
                height: 140rpx;
                margin-right: 20rpx;
                border-radius: 10rpx;
                overflow: hidden;
                border: 2rpx solid transparent;
                &.active {
                    border-color: #37bde6;
                }
                .template-img {
                    width: 100%;
                    height: 100%;
                }
            }
        }
    }
    .form-section {
        background: #fff;
        padding: 30rpx;
        .form-group {
            margin-bottom: 40rpx;
            .group-title {
                font-size: 32rpx;
                font-weight: bold;
                color: #333;
                margin-bottom: 20rpx;
            }
            .form-item {
                display: flex;
                align-items: center;
                padding: 20rpx 0;
                border-bottom: 1rpx solid #f0f0f0;
                .item-label {
                    width: 160rpx;
                    font-size: 28rpx;
                    color: #666;
                }
                .item-input {
                    flex: 1;
                    font-size: 28rpx;
                    color: #333;
                    padding: 0;
                }
                .item-textarea {
                    flex: 1;
                    font-size: 28rpx;
                    color: #333;
                    padding: 0;
                    height: 150rpx;
                    text-align: left;
                }
                .upload-area {
                    width: 150rpx;
                    height: 150rpx;
                    border-radius: 10rpx;
                    overflow: hidden;
                    background: #f5f5f5;
                    display: flex;
                    align-items: center;
                    justify-content: center;
                    .upload-img {
                        width: 100%;
                        height: 100%;
                    }
                    .upload-placeholder {
                        text-align: center;
                        .icon {
                            font-size: 48rpx;
                            color: #999;
                            margin-bottom: 10rpx;
                        }
                        text {
                            font-size: 24rpx;
                            color: #999;
                        }
                    }
            }
        }
        .submit-section {
            margin-top: 60rpx;
            margin-bottom: 40rpx;
            .submit-btn {
                width: 100%;
                background: #37bde6;
                color: #fff;
                border: none;
                border-radius: 10rpx;
                padding: 28rpx 0;
                font-size: 32rpx;
                font-weight: bold;
            }
        }
    }
</style>
mobile/pages/plus/business/components/visit-card.vue
New file
@@ -0,0 +1,176 @@
<template>
    <view class="visit-card">
        <view class="card-header">
            <image :src="visitInfo.avatar" mode="aspectFill" class="avatar"></image>
            <view class="header-info">
                <view class="visitor-name">{{visitInfo.name || '游客'}}</view>
                <view class="visit-time">{{formatTime(visitInfo.visit_time)}}</view>
            </view>
        </view>
        <view class="card-content">
            <view class="info-row" v-if="visitInfo.visit_count > 1">
                <text class="info-label">访问次数:</text>
                <text class="info-value">{{visitInfo.visit_count}}次</text>
            </view>
            <view class="info-row" v-if="visitInfo.device">
                <text class="info-label">使用设备:</text>
                <text class="info-value">{{visitInfo.device}}</text>
            </view>
            <view class="info-row" v-if="visitInfo.region">
                <text class="info-label">所在地区:</text>
                <text class="info-value">{{visitInfo.region}}</text>
            </view>
            <view class="info-row" v-if="visitInfo.source">
                <text class="info-label">来源渠道:</text>
                <text class="info-value">{{visitInfo.source}}</text>
            </view>
            <view class="action-buttons" v-if="showActions">
                <button class="btn-primary" @click="viewCard">查看名片</button>
                <button class="btn-secondary" @click="contactVisitor">联系访客</button>
            </view>
        </view>
    </view>
</template>
<script>
    export default {
        props: {
            visitInfo: {
                type: Object,
                default: () => ({
                    avatar: '',
                    name: '',
                    visit_time: '',
                    visit_count: 1,
                    device: '',
                    region: '',
                    source: ''
                })
            },
            showActions: {
                type: Boolean,
                default: true
            }
        },
        methods: {
            // 格式化时间
            formatTime(timeStr) {
                if (!timeStr) return '';
                const date = new Date(timeStr);
                const now = new Date();
                const diff = now - date;
                const days = Math.floor(diff / (1000 * 60 * 60 * 24));
                if (days === 0) {
                    const hours = Math.floor(diff / (1000 * 60 * 60));
                    if (hours === 0) {
                        const minutes = Math.floor(diff / (1000 * 60));
                        return minutes <= 1 ? '刚刚' : minutes + '分钟前';
                    } else {
                        return hours + '小时前';
                    }
                } else if (days === 1) {
                    return '昨天';
                } else if (days < 7) {
                    return days + '天前';
                } else {
                    return date.getMonth() + 1 + '月' + date.getDate() + '日';
                }
            },
            // 查看访客名片
            viewCard() {
                if (this.visitInfo.user_id) {
                    this.$emit('viewCard', this.visitInfo.user_id);
                }
            },
            // 联系访客
            contactVisitor() {
                this.$emit('contact', this.visitInfo);
            }
        }
    };
</script>
<style lang="scss">
    .visit-card {
        background: #fff;
        border-radius: 20rpx;
        padding: 20rpx;
        margin-bottom: 20rpx;
        .card-header {
            display: flex;
            align-items: center;
            padding-bottom: 20rpx;
            border-bottom: 1rpx solid #f0f0f0;
            .avatar {
                width: 80rpx;
                height: 80rpx;
                border-radius: 40rpx;
                margin-right: 20rpx;
            }
            .header-info {
                flex: 1;
                .visitor-name {
                    font-size: 32rpx;
                    font-weight: bold;
                    color: #333;
                    margin-bottom: 5rpx;
                }
                .visit-time {
                    font-size: 24rpx;
                    color: #999;
                }
            }
        }
        .card-content {
            padding-top: 20rpx;
            .info-row {
                display: flex;
                margin-bottom: 15rpx;
                .info-label {
                    font-size: 28rpx;
                    color: #666;
                    margin-right: 10rpx;
                }
                .info-value {
                    font-size: 28rpx;
                    color: #333;
                    flex: 1;
                }
            }
            .action-buttons {
                display: flex;
                margin-top: 20rpx;
                button {
                    flex: 1;
                    height: 70rpx;
                    line-height: 70rpx;
                    font-size: 28rpx;
                    border-radius: 35rpx;
                    margin: 0 10rpx;
                }
                .btn-primary {
                    background: #37bde6;
                    color: #fff;
                }
                .btn-secondary {
                    background: #f5f5f5;
                    color: #666;
                }
            }
        }
    }
</style>
mobile/pages/plus/business/detail.vue
New file
@@ -0,0 +1,448 @@
<template>
    <view>
        <header-bar title="名片详情" :isBack="true" @click="back"></header-bar>
        <scroll-view scroll-y="true" class="scroll-view">
            <view class="card-container" v-if="businessInfo">
                <!-- 名片头部 -->
                <view class="card-header" :style="{backgroundColor: backgroundColor}">
                    <image v-if="businessInfo.background_image" class="card-bg" :src="businessInfo.background_image" mode="aspectFill"></image>
                    <view class="header-content">
                        <image class="avatar" :src="businessInfo.avatar || '/static/default.png'" mode="aspectFill"></image>
                        <view class="user-info">
                            <view class="name">{{businessInfo.real_name}}</view>
                            <view class="company">{{businessInfo.company_name}}</view>
                            <view class="position">{{businessInfo.position}}</view>
                        </view>
                    </view>
                </view>
                <!-- 公司Logo -->
                <view class="logo-section" v-if="businessInfo.logo_image">
                    <image class="logo" :src="businessInfo.logo_image.file_path" mode="aspectFit"></image>
                </view>
                <!-- 联系信息 -->
                <view class="contact-section">
                    <view class="section-title">联系方式</view>
                    <view class="contact-item" @click="makePhoneCall(businessInfo.phone)">
                        <view class="item-left">
                            <view class="icon-circle">
                                <text class="icon iconfont icon-phone"></text>
                            </view>
                            <text class="item-label">电话</text>
                        </view>
                        <view class="item-right">
                            <text class="item-value">{{businessInfo.phone}}</text>
                            <text class="icon iconfont icon-jiantou"></text>
                        </view>
                    </view>
                    <view class="contact-item" v-if="businessInfo.mobile" @click="makePhoneCall(businessInfo.mobile)">
                        <view class="item-left">
                            <view class="icon-circle">
                                <text class="icon iconfont icon-mobile"></text>
                            </view>
                            <text class="item-label">手机</text>
                        </view>
                        <view class="item-right">
                            <text class="item-value">{{businessInfo.mobile}}</text>
                            <text class="icon iconfont icon-jiantou"></text>
                        </view>
                    </view>
                    <view class="contact-item" v-if="businessInfo.email" @click="copyEmail(businessInfo.email)">
                        <view class="item-left">
                            <view class="icon-circle">
                                <text class="icon iconfont icon-email"></text>
                            </view>
                            <text class="item-label">邮箱</text>
                        </view>
                        <view class="item-right">
                            <text class="item-value">{{businessInfo.email}}</text>
                            <text class="icon iconfont icon-jiantou"></text>
                        </view>
                    </view>
                    <view class="contact-item" v-if="businessInfo.address" @click="openLocation(businessInfo.address)">
                        <view class="item-left">
                            <view class="icon-circle">
                                <text class="icon iconfont icon-location"></text>
                            </view>
                            <text class="item-label">地址</text>
                        </view>
                        <view class="item-right">
                            <text class="item-value">{{businessInfo.address}}</text>
                            <text class="icon iconfont icon-jiantou"></text>
                        </view>
                    </view>
                </view>
                <!-- 详细信息 -->
                <view class="detail-section">
                    <view class="section-title">详细信息</view>
                    <view class="detail-item" v-if="businessInfo.intro">
                        <view class="detail-label">个人简介</view>
                        <view class="detail-content">{{businessInfo.intro}}</view>
                    </view>
                    <view class="detail-item" v-if="businessInfo.business_scope">
                        <view class="detail-label">业务范围</view>
                        <view class="detail-content">{{businessInfo.business_scope}}</view>
                    </view>
                </view>
            </view>
            <!-- 底部操作按钮 -->
            <view class="action-section">
                <view class="action-left">
                    <view class="action-btn" @click="saveCard" :class="{active: isSaved}">
                        <text class="icon iconfont" :class="isSaved ? 'icon-success' : 'icon-star'"></text>
                        <text>{{isSaved ? '已保存' : '收藏'}}</text>
                    </view>
                </view>
                <view class="action-right">
                    <view class="share-btn" @click="shareCard">分享名片</view>
                </view>
            </view>
        </scroll-view>
    </view>
</template>
<script>
    export default {
        data() {
            return {
                businessInfo: null,
                business_card_id: '',
                isSaved: false,
                backgroundColor: '#37bde6',
                loading: true
            };
        },
        onLoad(options) {
            if (options.business_card_id) {
                this.business_card_id = options.business_card_id;
                this.getBusinessDetail();
                this.checkSavedStatus();
                // 记录访问日志
                this.recordVisit();
            }
        },
        methods: {
            back() {
                uni.navigateBack();
            },
            // 获取名片详情
            getBusinessDetail() {
                let _this = this;
                _this.loading = true;
                _this._post('plus.business/business/detail', { business_card_id: _this.business_card_id }, function(res) {
                    _this.businessInfo = res.data;
                    // 设置背景色
                    if (_this.businessInfo.template && _this.businessInfo.template.style?.background?.color) {
                        _this.backgroundColor = _this.businessInfo.template.style.background.color;
                    }
                    _this.loading = false;
                });
            },
            // 检查是否已保存
            checkSavedStatus() {
                let _this = this;
                _this._post('plus.business/saving/check', { business_card_id: _this.business_card_id }, function(res) {
                    _this.isSaved = res.data.is_saved;
                });
            },
            // 记录访问
            recordVisit() {
                let _this = this;
                const params = {
                    business_card_id: _this.business_card_id
                };
                // 如果有推荐人ID,也记录下来
                if (this.$route.query.referee_id) {
                    params.referee_id = this.$route.query.referee_id;
                }
                _this._post('plus.business/business/recordVisit', params, function() {
                    // 无需处理返回结果
                });
            },
            // 拨打电话
            makePhoneCall(phone) {
                uni.makePhoneCall({
                    phoneNumber: phone
                });
            },
            // 复制邮箱
            copyEmail(email) {
                uni.setClipboardData({
                    data: email,
                    success: () => {
                        this.showSuccess('邮箱已复制');
                    }
                });
            },
            // 打开地图
            openLocation(address) {
                // 这里简化处理,实际项目中可能需要调用地图API进行地理编码
                uni.openLocation({
                    latitude: 0,
                    longitude: 0,
                    name: address,
                    address: address,
                    scale: 18
                });
            },
            // 保存名片
            saveCard() {
                let _this = this;
                _this._post('plus.business/saving/save', { business_card_id: _this.business_card_id }, function(res) {
                    _this.isSaved = !_this.isSaved;
                    _this.showSuccess(_this.isSaved ? '保存成功' : '取消保存');
                });
            },
            // 分享名片
            shareCard() {
                uni.showShareMenu({
                    withShareTicket: true,
                    menus: ['shareAppMessage', 'shareTimeline']
                });
            }
        },
        onShareAppMessage() {
            if (this.businessInfo) {
                return {
                    title: `${this.businessInfo.real_name}的电子名片`,
                    path: `/pages/plus/business/detail?business_card_id=${this.business_card_id}&referee_id=${this.getUserId()}`
                };
            }
            return {
                title: '电子名片',
                path: `/pages/plus/business/detail?business_card_id=${this.business_card_id}`
            };
        },
        onShareTimeline() {
            if (this.businessInfo) {
                return {
                    title: `${this.businessInfo.real_name}的电子名片`,
                    path: `/pages/plus/business/detail?business_card_id=${this.business_card_id}&referee_id=${this.getUserId()}`
                };
            }
            return {
                title: '电子名片',
                path: `/pages/plus/business/detail?business_card_id=${this.business_card_id}`
            };
        }
    };
</script>
<style lang="scss">
    .scroll-view {
        height: calc(100vh - 80rpx);
    }
    .card-header {
        position: relative;
        color: #fff;
        padding: 40rpx;
        .card-bg {
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            z-index: 1;
            opacity: 0.9;
        }
        .header-content {
            position: relative;
            z-index: 2;
            display: flex;
            align-items: center;
            .avatar {
                width: 180rpx;
                height: 180rpx;
                border-radius: 50%;
                border: 6rpx solid rgba(255, 255, 255, 0.8);
                background: #fff;
            }
            .user-info {
                margin-left: 30rpx;
                .name {
                    font-size: 48rpx;
                    font-weight: bold;
                    margin-bottom: 12rpx;
                }
                .company {
                    font-size: 34rpx;
                    margin-bottom: 8rpx;
                    opacity: 0.9;
                }
                .position {
                    font-size: 30rpx;
                    opacity: 0.8;
                }
            }
        }
    }
    .logo-section {
        background: #fff;
        padding: 30rpx 0;
        display: flex;
        justify-content: center;
        .logo {
            width: 150rpx;
            height: 150rpx;
            border-radius: 10rpx;
        }
    }
    .contact-section,
    .detail-section {
        background: #fff;
        margin-top: 20rpx;
        padding: 30rpx;
        .section-title {
            font-size: 32rpx;
            font-weight: bold;
            color: #333;
            margin-bottom: 20rpx;
            padding-bottom: 20rpx;
            border-bottom: 1rpx solid #f0f0f0;
        }
        .contact-item {
            display: flex;
            justify-content: space-between;
            align-items: center;
            padding: 25rpx 0;
            border-bottom: 1rpx solid #f0f0f0;
            &:last-child {
                border-bottom: none;
            }
            .item-left {
                display: flex;
                align-items: center;
                .icon-circle {
                    width: 60rpx;
                    height: 60rpx;
                    border-radius: 50%;
                    background: #f5f5f5;
                    display: flex;
                    align-items: center;
                    justify-content: center;
                    margin-right: 20rpx;
                    .icon {
                        font-size: 36rpx;
                        color: #37bde6;
                    }
                }
                .item-label {
                    font-size: 30rpx;
                    color: #666;
                }
            }
            .item-right {
                display: flex;
                align-items: center;
                .item-value {
                    font-size: 30rpx;
                    color: #333;
                    margin-right: 10rpx;
                }
                .icon {
                    font-size: 24rpx;
                    color: #999;
                }
            }
        }
        .detail-item {
            margin-bottom: 30rpx;
            .detail-label {
                font-size: 30rpx;
                font-weight: bold;
                color: #666;
                margin-bottom: 15rpx;
            }
            .detail-content {
                font-size: 28rpx;
                color: #333;
                line-height: 1.6;
            }
        }
    }
    .action-section {
        position: fixed;
        bottom: 0;
        left: 0;
        width: 100%;
        background: #fff;
        box-shadow: 0 -2rpx 20rpx rgba(0, 0, 0, 0.1);
        display: flex;
        padding: 20rpx;
        z-index: 999;
        .action-left {
            flex: 1;
            display: flex;
            align-items: center;
            .action-btn {
                display: flex;
                align-items: center;
                padding: 15rpx 30rpx;
                border-radius: 30rpx;
                border: 2rpx solid #37bde6;
                color: #37bde6;
                &.active {
                    background: #37bde6;
                    color: #fff;
                }
                .icon {
                    font-size: 28rpx;
                    margin-right: 10rpx;
                }
            }
        }
        .action-right {
            flex: 2;
            .share-btn {
                width: 100%;
                background: linear-gradient(90deg, #44bbff, #2b81ff);
                color: #fff;
                text-align: center;
                padding: 24rpx 0;
                border-radius: 30rpx;
                font-size: 32rpx;
                font-weight: bold;
            }
        }
    }
</style>
mobile/pages/plus/business/index.vue
New file
@@ -0,0 +1,507 @@
<template>
    <view>
        <header-bar title="我的名片" :isBack="true" @click="back"></header-bar>
        <!-- 名片展示区域 -->
        <view class="content">
            <view class="business-card" v-if="businessList.length > 0">
                <image class="top-image" :src="businessList[current].background_image" mode="aspectFill"></image>
                <view class="business-info">
                    <image class="avatar" :src="businessList[current].avatar" mode="aspectFill"></image>
                    <view class="info-text">
                        <view class="name">{{businessList[current].real_name}}</view>
                        <view class="company">{{businessList[current].company_name}}</view>
                        <view class="position">{{businessList[current].position}}</view>
                    </view>
                </view>
                <view class="business-contact">
                    <view class="contact-item" @click="makePhoneCall(businessList[current].phone)">
                        <text class="icon iconfont icon-phone"></text>
                        <text class="text">{{businessList[current].phone}}</text>
                    </view>
                    <view class="contact-item" v-if="businessList[current].mobile"
                        @click="makePhoneCall(businessList[current].mobile)">
                        <text class="icon iconfont icon-mobile"></text>
                        <text class="text">{{businessList[current].mobile}}</text>
                    </view>
                    <view class="contact-item" v-if="businessList[current].email"
                        @click="sendEmail(businessList[current].email)">
                        <text class="icon iconfont icon-email"></text>
                        <text class="text">{{businessList[current].email}}</text>
                    </view>
                </view>
            </view>
            <!-- 名片操作按钮 -->
            <view class="action-buttons">
                <view class="btn" @click="editCard()">编辑名片</view>
                <view class="btn" @click="switchCard()">切换名片</view>
                <view class="btn" @click="shareCard()">分享名片</view>
            </view>
            <!-- 数据统计区域 -->
            <view class="statistics">
                <view class="stat-item">
                    <view class="number">{{statistics.view_count || 0}}</view>
                    <view class="label">浏览次数</view>
                </view>
                <view class="stat-item">
                    <view class="number">{{statistics.save_count || 0}}</view>
                    <view class="label">保存次数</view>
                </view>
                <view class="stat-item">
                    <view class="number">{{statistics.share_count || 0}}</view>
                    <view class="label">分享次数</view>
                </view>
            </view>
            <!-- 最近访客 -->
            <view class="visitors">
                <view class="section-title">
                    <text>最近访客</text>
                    <text class="more" @click="viewAllVisitors()">查看全部</text>
                </view>
                <view class="visitor-list">
                    <view class="visitor-item" v-for="(visitor, index) in visitors" :key="index">
                        <image class="visitor-avatar" :src="visitor.avatar || '/static/default.png'" mode="aspectFill">
                        </image>
                        <view class="visitor-info">
                            <view class="visitor-name">{{visitor.user_name || '未知访客'}}</view>
                            <view class="visitor-time">{{formatTime(visitor.visit_time)}}</view>
                        </view>
                    </view>
                </view>
            </view>
            <!-- 切换名片弹窗 -->
            <uni-popup ref="popup" type="bottom" :mask-click="false">
                <view class="popup-content">
                    <view class="popup-header">
                        <text class="title">选择名片</text>
                        <text class="close" @click="closePopup">×</text>
                    </view>
                    <scroll-view scroll-y="true" class="card-scroll">
                        <view class="card-item" v-for="(card, index) in businessList" :key="index"
                            :class="{active: index === current}" @click="selectCard(index)">
                            <view class="card-preview">
                                <view class="card-name">{{card.real_name}}</view>
                                <view class="card-company">{{card.company_name}}</view>
                            </view>
                            <text v-if="index === current" class="icon iconfont icon-check"></text>
                        </view>
                        <view class="add-card" @click="addNewCard()">
                            <text class="icon iconfont icon-add"></text>
                            <text>添加新名片</text>
                        </view>
                    </scroll-view>
                </view>
            </uni-popup>
        </view>
    </view>
</template>
<script>
    export default {
        data() {
            return {
                businessList: [],
                current: 0,
                statistics: {},
                visitors: [],
                loading: false,
                page: 1,
                search: ''
            };
        },
        onLoad() {
            this.init();
        },
        onShow() {
            this.getbusinessList();
            this.getVisitorList();
        },
        methods: {
            back() {
                uni.navigateBack();
            },
            init() {
                this.getbusinessList();
                this.getStatistics();
                this.getVisitorList();
            },
            getbusinessList() {
                let _this = this;
                _this._post('plus.business/business/getList', {}, function(res) {
                    _this.businessList = res.data;
                    if (_this.businessList.length > 0) {
                        _this.getCardStatistics(_this.businessList[_this.current].business_card_id);
                    }
                });
            },
            getCardStatistics(business_card_id) {
                let _this = this;
                _this._post('plus.business/business/getStatistics', {
                    business_card_id: business_card_id
                }, function(res) {
                    _this.statistics = res.data;
                });
            },
            getStatistics() {
                // 获取统计数据
                let _this = this;
                _this._post('plus.business/business/getStatistics', {}, function(res) {
                    _this.statistics = res.data;
                });
            },
            getVisitorList() {
                let _this = this;
                _this._post('plus.business/business/getVisitors', {
                    page: 1,
                    list_rows: 10
                }, function(res) {
                    _this.visitors = res.data.list;
                });
            },
            makePhoneCall(phone) {
                uni.makePhoneCall({
                    phoneNumber: phone
                });
            },
            sendEmail(email) {
                uni.setClipboardData({
                    data: email,
                    success: () => {
                        this.showSuccess('邮箱已复制');
                    }
                });
            },
            editCard() {
                if (this.businessList.length > 0) {
                    this.gotoPage(
                        `/pages/plus/business/add?business_card_id=${this.businessList[this.current].business_card_id}`
                        );
                }
            },
            switchCard() {
                this.$refs.popup.open();
            },
            closePopup() {
                this.$refs.popup.close();
            },
            selectCard(index) {
                this.current = index;
                this.getCardStatistics(this.businessList[this.current].business_card_id);
                this.closePopup();
            },
            addNewCard() {
                this.closePopup();
                this.gotoPage('/pages/plus/business/add');
            },
            shareCard() {
                // 分享名片逻辑
                if (this.businessList.length > 0) {
                    uni.showShareMenu({
                        withShareTicket: true,
                        menus: ['shareAppMessage', 'shareTimeline']
                    });
                }
            },
            viewAllVisitors() {
                // 查看全部访客
                this.gotoPage('/pages/plus/business/visitors');
            },
            formatTime(time) {
                if (!time) return '';
                const date = new Date(time);
                const now = new Date();
                const diff = now - date;
                const days = Math.floor(diff / (1000 * 60 * 60 * 24));
                if (days === 0) {
                    return '今天 ' + date.getHours() + ':' + (date.getMinutes() < 10 ? '0' : '') + date.getMinutes();
                } else if (days === 1) {
                    return '昨天 ' + date.getHours() + ':' + (date.getMinutes() < 10 ? '0' : '') + date.getMinutes();
                } else if (days < 7) {
                    return days + '天前';
                } else {
                    return date.getFullYear() + '-' + (date.getMonth() + 1) + '-' + date.getDate();
                }
            }
        },
        onShareAppMessage() {
            if (this.businessList.length > 0) {
                return {
                    title: `${this.businessList[this.current].real_name}的电子名片`,
                    path: `/pages/plus/business/detail?business_card_id=${this.businessList[this.current].business_card_id}&referee_id=${this.getUserId()}`
                };
            }
            return {
                title: '电子名片',
                path: '/pages/plus/business/index'
            };
        },
        onShareTimeline() {
            if (this.businessList.length > 0) {
                return {
                    title: `${this.businessList[this.current].real_name}的电子名片`,
                    path: `/pages/plus/business/detail?business_card_id=${this.businessList[this.current].business_card_id}&referee_id=${this.getUserId()}`
                };
            }
            return {
                title: '电子名片',
                path: '/pages/plus/business/index'
            };
        }
    };
</script>
<style lang="scss">
    .content {
        padding: 20rpx;
    }
    .business-card {
        background: #fff;
        border-radius: 20rpx;
        overflow: hidden;
        padding-bottom: 30rpx;
        .top-image {
            width: 100%;
            height: 300rpx;
            display: block;
        }
        .business-info {
            padding: 20rpx;
            display: flex;
            align-items: center;
            .avatar {
                width: 150rpx;
                height: 150rpx;
                border-radius: 50%;
                border: 4rpx solid #fff;
                box-shadow: 0 0 10rpx rgba(0, 0, 0, 0.1);
            }
            .info-text {
                margin-left: 20rpx;
                flex: 1;
                .name {
                    font-size: 40rpx;
                    font-weight: bold;
                    margin-bottom: 8rpx;
                }
                .company {
                    font-size: 28rpx;
                    color: #666;
                    margin-bottom: 4rpx;
                }
                .position {
                    font-size: 26rpx;
                    color: #999;
                }
            }
        }
        .business-contact {
            padding: 0 20rpx;
            .contact-item {
                display: flex;
                align-items: center;
                padding: 16rpx 0;
                border-bottom: 1rpx solid #f0f0f0;
                .icon {
                    font-size: 32rpx;
                    color: #37bde6;
                    margin-right: 16rpx;
                }
                .text {
                    font-size: 28rpx;
                    color: #333;
                }
            }
        }
    }
    .action-buttons {
        display: flex;
        justify-content: space-between;
        margin: 30rpx 0;
        .btn {
            flex: 1;
            background: #37bde6;
            color: #fff;
            text-align: center;
            padding: 24rpx 0;
            border-radius: 10rpx;
            font-size: 30rpx;
            margin: 0 10rpx;
        }
    }
    .statistics {
        background: #fff;
        border-radius: 20rpx;
        padding: 30rpx 0;
        display: flex;
        justify-content: space-around;
        .stat-item {
            display: flex;
            flex-direction: column;
            align-items: center;
            .number {
                font-size: 40rpx;
                font-weight: bold;
                color: #333;
                margin-bottom: 8rpx;
            }
            .label {
                font-size: 26rpx;
                color: #999;
            }
        }
    }
    .visitors {
        background: #fff;
        border-radius: 20rpx;
        margin-top: 30rpx;
        padding: 20rpx;
        .section-title {
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-bottom: 20rpx;
            .title {
                font-size: 32rpx;
                font-weight: bold;
                color: #333;
            }
            .more {
                font-size: 26rpx;
                color: #37bde6;
            }
        }
        .visitor-item {
            display: flex;
            align-items: center;
            padding: 20rpx 0;
            border-bottom: 1rpx solid #f0f0f0;
            &:last-child {
                border-bottom: none;
            }
            .visitor-avatar {
                width: 80rpx;
                height: 80rpx;
                border-radius: 50%;
            }
            .visitor-info {
                margin-left: 20rpx;
                flex: 1;
                .visitor-name {
                    font-size: 28rpx;
                    color: #333;
                    margin-bottom: 4rpx;
                }
                .visitor-time {
                    font-size: 24rpx;
                    color: #999;
                }
            }
        }
    }
    .popup-content {
        background: #fff;
        border-top-left-radius: 30rpx;
        border-top-right-radius: 30rpx;
        .popup-header {
            display: flex;
            justify-content: space-between;
            align-items: center;
            padding: 20rpx 30rpx;
            border-bottom: 1rpx solid #f0f0f0;
            .title {
                font-size: 32rpx;
                font-weight: bold;
                color: #333;
            }
            .close {
                font-size: 40rpx;
                color: #999;
                padding: 0 20rpx;
            }
        }
        .card-scroll {
            height: 500rpx;
            .card-item {
                display: flex;
                justify-content: space-between;
                align-items: center;
                padding: 30rpx;
                border-bottom: 1rpx solid #f0f0f0;
                &.active {
                    background: #f5f5f5;
                }
                .card-preview {
                    .card-name {
                        font-size: 32rpx;
                        font-weight: bold;
                        color: #333;
                        margin-bottom: 8rpx;
                    }
                    .card-company {
                        font-size: 26rpx;
                        color: #666;
                    }
                }
                .icon {
                    font-size: 32rpx;
                    color: #37bde6;
                }
            }
            .add-card {
                display: flex;
                align-items: center;
                justify-content: center;
                padding: 30rpx;
                color: #37bde6;
                font-size: 30rpx;
                .icon {
                    font-size: 36rpx;
                    margin-right: 10rpx;
                }
            }
        }
    }
</style>
mobile/pages/plus/business/information.vue
New file
@@ -0,0 +1,437 @@
<template>
    <view>
        <header-bar title="数据统计" :isBack="true" @click="back"></header-bar>
        <view class="content">
            <!-- 数据概览 -->
            <view class="overview-section">
                <view class="overview-item">
                    <view class="number">{{totalViews}}</view>
                    <view class="label">总浏览量</view>
                </view>
                <view class="overview-item">
                    <view class="number">{{totalSaves}}</view>
                    <view class="label">总保存量</view>
                </view>
                <view class="overview-item">
                    <view class="number">{{totalShares}}</view>
                    <view class="label">总分享量</view>
                </view>
            </view>
            <!-- 趋势图表区域 -->
            <view class="chart-section">
                <view class="chart-header">
                    <view class="chart-title">数据趋势</view>
                    <view class="chart-tabs">
                        <view class="tab" :class="{active: timeRange === 'week'}" @click="setTimeRange('week')">周</view>
                        <view class="tab" :class="{active: timeRange === 'month'}" @click="setTimeRange('month')">月</view>
                        <view class="tab" :class="{active: timeRange === 'year'}" @click="setTimeRange('year')">年</view>
                    </view>
                </view>
                <view class="chart-content">
                    <!-- 这里可以集成图表组件,如echarts等 -->
                    <view class="chart-placeholder" v-if="!chartData.length">
                        <text>暂无数据</text>
                    </view>
                    <view class="chart-list" v-else>
                        <view class="chart-item" v-for="(item, index) in chartData" :key="index">
                            <view class="chart-bar">
                                <view class="bar" :style="{height: getBarHeight(item.views)}">
                                    <view class="bar-value">{{item.views}}</view>
                                </view>
                            </view>
                            <view class="chart-label">{{item.date}}</view>
                        </view>
                    </view>
                </view>
            </view>
            <!-- 详细统计数据 -->
            <view class="detail-section">
                <view class="section-title">详细统计</view>
                <!-- 访问来源 -->
                <view class="stat-card">
                    <view class="card-title">访问来源</view>
                    <view class="source-list">
                        <view class="source-item" v-for="(source, index) in visitSources" :key="index">
                            <view class="source-name">{{source.name}}</view>
                            <view class="source-value">{{source.value}}次</view>
                            <view class="source-progress">
                                <view class="progress-bar" :style="{width: source.percentage + '%'}"></view>
                            </view>
                        </view>
                    </view>
                </view>
                <!-- 地域分布 -->
                <view class="stat-card">
                    <view class="card-title">地域分布</view>
                    <view class="region-list">
                        <view class="region-item" v-for="(region, index) in regions" :key="index">
                            <view class="region-name">{{region.name}}</view>
                            <view class="region-value">{{region.value}}人</view>
                        </view>
                    </view>
                </view>
                <!-- 名片效果对比 -->
                <view class="stat-card" v-if="cardComparison.length > 1">
                    <view class="card-title">名片效果对比</view>
                    <view class="comparison-list">
                        <view class="comparison-item" v-for="(card, index) in cardComparison" :key="index">
                            <view class="comparison-header">
                                <view class="card-name">{{card.name}}</view>
                                <view class="card-view">
                                    <text class="icon iconfont icon-eye"></text>
                                    <text>{{card.views}}次</text>
                                </view>
                            </view>
                            <view class="comparison-details">
                                <view class="detail">
                                    <text>保存率</text>
                                    <text class="rate">{{card.saveRate}}%</text>
                                </view>
                                <view class="detail">
                                    <text>分享率</text>
                                    <text class="rate">{{card.shareRate}}%</text>
                                </view>
                            </view>
                        </view>
                    </view>
                </view>
            </view>
        </view>
    </view>
</template>
<script>
    export default {
        data() {
            return {
                timeRange: 'week', // week, month, year
                totalViews: 0,
                totalSaves: 0,
                totalShares: 0,
                chartData: [],
                visitSources: [],
                regions: [],
                cardComparison: [],
                currentCardId: ''
            };
        },
        onLoad(options) {
            if (options.current) {
                this.currentCardId = options.current;
            }
            this.getData();
        },
        methods: {
            back() {
                uni.navigateBack();
            },
            // 获取所有统计数据
            getData() {
                this.getOverview();
                this.getChartData();
                this.getDetailedStats();
            },
            // 获取概览数据
            getOverview() {
                let _this = this;
                const params = {};
                if (_this.currentCardId) {
                    params.business_card_id = _this.currentCardId;
                }
                _this._post('plus.business/business/getOverview', params, function(res) {
                    _this.totalViews = res.data.views || 0;
                    _this.totalSaves = res.data.saves || 0;
                    _this.totalShares = res.data.shares || 0;
                });
            },
            // 获取图表数据
            getChartData() {
                let _this = this;
                _this._post('plus.business/business/getChartData', {
                    timeRange: _this.timeRange,
                    business_card_id: _this.currentCardId
                }, function(res) {
                    _this.chartData = res.data || [];
                });
            },
            // 获取详细统计
            getDetailedStats() {
                let _this = this;
                const params = {
                    business_card_id: _this.currentCardId
                };
                // 获取访问来源
                _this._post('plus.business/business/getVisitSources', params, function(res) {
                    _this.visitSources = res.data || [];
                });
                // 获取地域分布
                _this._post('plus.business/business/getRegions', params, function(res) {
                    _this.regions = res.data || [];
                });
                // 获取名片对比数据
                _this._post('plus.business/business/getCardComparison', {}, function(res) {
                    _this.cardComparison = res.data || [];
                });
            },
            // 设置时间范围
            setTimeRange(range) {
                this.timeRange = range;
                this.getChartData();
            },
            // 获取柱状图高度
            getBarHeight(value) {
                // 简单计算柱状图高度,实际项目中可能需要更复杂的计算
                const maxValue = Math.max(...this.chartData.map(item => item.views));
                if (maxValue === 0) return '0%';
                const height = (value / maxValue) * 100;
                return Math.max(height, 10) + '%'; // 最小高度为10%
            }
        }
    };
</script>
<style lang="scss">
    .content {
        padding: 20rpx;
    }
    .overview-section {
        display: flex;
        background: #fff;
        border-radius: 20rpx;
        padding: 30rpx 0;
        margin-bottom: 20rpx;
        .overview-item {
            flex: 1;
            display: flex;
            flex-direction: column;
            align-items: center;
            .number {
                font-size: 48rpx;
                font-weight: bold;
                color: #37bde6;
                margin-bottom: 10rpx;
            }
            .label {
                font-size: 28rpx;
                color: #666;
            }
        }
    }
    .chart-section {
        background: #fff;
        border-radius: 20rpx;
        padding: 30rpx;
        margin-bottom: 20rpx;
        .chart-header {
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-bottom: 30rpx;
            .chart-title {
                font-size: 32rpx;
                font-weight: bold;
                color: #333;
            }
            .chart-tabs {
                display: flex;
                .tab {
                    padding: 8rpx 20rpx;
                    margin-left: 10rpx;
                    font-size: 26rpx;
                    color: #666;
                    border-radius: 20rpx;
                    &.active {
                        background: #37bde6;
                        color: #fff;
                    }
                }
            }
        }
        .chart-placeholder {
            height: 300rpx;
            display: flex;
            align-items: center;
            justify-content: center;
            color: #999;
            font-size: 28rpx;
        }
        .chart-list {
            display: flex;
            align-items: flex-end;
            justify-content: space-between;
            height: 300rpx;
            padding: 20rpx 0;
            .chart-item {
                flex: 1;
                display: flex;
                flex-direction: column;
                align-items: center;
                .chart-bar {
                    flex: 1;
                    display: flex;
                    align-items: flex-end;
                    width: 60rpx;
                    .bar {
                        background: linear-gradient(to top, #37bde6, #44bbff);
                        width: 100%;
                        border-radius: 6rpx 6rpx 0 0;
                        display: flex;
                        align-items: flex-start;
                        justify-content: center;
                        padding-top: 10rpx;
                        .bar-value {
                            font-size: 22rpx;
                            color: #666;
                        }
                    }
                }
                .chart-label {
                    font-size: 22rpx;
                    color: #999;
                    margin-top: 10rpx;
                    transform: rotate(-45deg);
                    white-space: nowrap;
                }
            }
        }
    }
    .detail-section {
        .section-title {
            font-size: 32rpx;
            font-weight: bold;
            color: #333;
            margin-bottom: 20rpx;
        }
        .stat-card {
            background: #fff;
            border-radius: 20rpx;
            padding: 30rpx;
            margin-bottom: 20rpx;
            .card-title {
                font-size: 28rpx;
                font-weight: bold;
                color: #666;
                margin-bottom: 20rpx;
            }
            .source-list,
            .region-list {
                .source-item,
                .region-item {
                    margin-bottom: 20rpx;
                    &:last-child {
                        margin-bottom: 0;
                    }
                    .source-name,
                    .region-name {
                        font-size: 28rpx;
                        color: #333;
                        margin-bottom: 10rpx;
                    }
                    .source-value,
                    .region-value {
                        font-size: 26rpx;
                        color: #999;
                        margin-bottom: 10rpx;
                    }
                    .source-progress {
                        height: 12rpx;
                        background: #f0f0f0;
                        border-radius: 6rpx;
                        .progress-bar {
                            height: 100%;
                            background: #37bde6;
                            border-radius: 6rpx;
                        }
                    }
                }
            }
            .comparison-list {
                .comparison-item {
                    padding: 20rpx;
                    background: #f5f5f5;
                    border-radius: 10rpx;
                    margin-bottom: 20rpx;
                    &:last-child {
                        margin-bottom: 0;
                    }
                    .comparison-header {
                        display: flex;
                        justify-content: space-between;
                        align-items: center;
                        margin-bottom: 15rpx;
                        .card-name {
                            font-size: 30rpx;
                            font-weight: bold;
                            color: #333;
                        }
                        .card-view {
                            display: flex;
                            align-items: center;
                            font-size: 26rpx;
                            color: #666;
                            .icon {
                                margin-right: 8rpx;
                            }
                        }
                    }
                    .comparison-details {
                        display: flex;
                        .detail {
                            margin-right: 40rpx;
                            font-size: 26rpx;
                            color: #666;
                            .rate {
                                margin-left: 10rpx;
                                color: #37bde6;
                                font-weight: bold;
                            }
                        }
                    }
                }
            }
        }
    }
</style>
mobile/pages/plus/business/share.vue
New file
@@ -0,0 +1,390 @@
<template>
    <view>
        <header-bar title="分享设置" :isBack="true" @click="back"></header-bar>
        <view class="content">
            <!-- 分享内容设置 -->
            <view class="setting-section">
                <view class="section-title">分享内容设置</view>
                <view class="setting-card">
                    <view class="setting-item">
                        <view class="setting-label">分享标题</view>
                        <view class="setting-control">
                            <input type="text" class="input" v-model="shareConfig.title" placeholder="请输入分享标题" />
                        </view>
                    </view>
                    <view class="setting-item">
                        <view class="setting-label">分享描述</view>
                        <view class="setting-control">
                            <textarea class="textarea" v-model="shareConfig.desc" placeholder="请输入分享描述"></textarea>
                        </view>
                    </view>
                    <view class="setting-item">
                        <view class="setting-label">分享图片</view>
                        <view class="setting-control">
                            <view class="image-upload">
                                <image v-if="shareConfig.image" :src="shareConfig.image" mode="aspectFill" class="uploaded-image"></image>
                                <view v-else class="upload-placeholder" @click="uploadShareImage">
                                    <text class="icon iconfont icon-upload"></text>
                                    <text>上传分享图片</text>
                                </view>
                            </view>
                            <view class="image-hint">建议尺寸: 300*300px,不超过2MB</view>
                        </view>
                    </view>
                </view>
            </view>
            <!-- 分享样式设置 -->
            <view class="setting-section">
                <view class="section-title">分享样式设置</view>
                <view class="setting-card">
                    <view class="setting-item">
                        <view class="setting-label">分享卡片样式</view>
                        <view class="setting-control">
                            <view class="card-style-list">
                                <view
                                    class="card-style-item"
                                    :class="{active: shareConfig.style === 'style1'}"
                                    @click="shareConfig.style = 'style1'"
                                >
                                    <image src="../../../../static/images/card-style1.png" mode="aspectFit"></image>
                                    <view class="check-icon" v-if="shareConfig.style === 'style1'"></view>
                                </view>
                                <view
                                    class="card-style-item"
                                    :class="{active: shareConfig.style === 'style2'}"
                                    @click="shareConfig.style = 'style2'"
                                >
                                    <image src="../../../../static/images/card-style2.png" mode="aspectFit"></image>
                                    <view class="check-icon" v-if="shareConfig.style === 'style2'"></view>
                                </view>
                                <view
                                    class="card-style-item"
                                    :class="{active: shareConfig.style === 'style3'}"
                                    @click="shareConfig.style = 'style3'"
                                >
                                    <image src="../../../../static/images/card-style3.png" mode="aspectFit"></image>
                                    <view class="check-icon" v-if="shareConfig.style === 'style3'"
                                </view>
                            </view>
                        </view>
                    </view>
                    <view class="setting-item">
                        <view class="setting-label">显示联系方式</view>
                        <view class="setting-control">
                            <switch :checked="shareConfig.showContact" @change="shareConfig.showContact = !shareConfig.showContact" />
                        </view>
                    </view>
                    <view class="setting-item">
                        <view class="setting-label">显示公司信息</view>
                        <view class="setting-control">
                            <switch :checked="shareConfig.showCompany" @change="shareConfig.showCompany = !shareConfig.showCompany" />
                        </view>
                    </view>
                </view>
            </view>
            <!-- 分享权限设置 -->
            <view class="setting-section">
                <view class="section-title">分享权限设置</view>
                <view class="setting-card">
                    <view class="setting-item">
                        <view class="setting-label">允许保存名片</view>
                        <view class="setting-control">
                            <switch :checked="shareConfig.allowSave" @change="shareConfig.allowSave = !shareConfig.allowSave" />
                        </view>
                    </view>
                    <view class="setting-item">
                        <view class="setting-label">允许二次分享</view>
                        <view class="setting-control">
                            <switch :checked="shareConfig.allowReshare" @change="shareConfig.allowReshare = !shareConfig.allowReshare" />
                        </view>
                    </view>
                    <view class="setting-item">
                        <view class="setting-label">分享有效期</view>
                        <view class="setting-control">
                            <picker mode="selector" range="['永久有效', '7天', '30天', '90天']" v-model="shareConfig.expiryIndex" @change="onExpiryChange">
                                <view class="picker">{{getExpiryText()}}</view>
                            </picker>
                        </view>
                    </view>
                </view>
            </view>
            <!-- 保存按钮 -->
            <view class="save-btn-container">
                <button class="save-btn" @click="saveConfig">保存设置</button>
            </view>
        </view>
    </view>
</template>
<script>
    export default {
        data() {
            return {
                currentCardId: '',
                shareConfig: {
                    title: '',
                    desc: '',
                    image: '',
                    style: 'style1',
                    showContact: true,
                    showCompany: true,
                    allowSave: true,
                    allowReshare: true,
                    expiryIndex: 0
                }
            };
        },
        onLoad(options) {
            if (options.current) {
                this.currentCardId = options.current;
            }
            this.getShareConfig();
        },
        methods: {
            back() {
                uni.navigateBack();
            },
            // 获取分享配置
            getShareConfig() {
                let _this = this;
                _this._post('plus.business/business/getShareConfig', {
                    business_card_id: _this.currentCardId
                }, function(res) {
                    if (res.data) {
                        _this.shareConfig = {
                            title: res.data.title || '',
                            desc: res.data.desc || '',
                            image: res.data.image || '',
                            style: res.data.style || 'style1',
                            showContact: res.data.show_contact === 1,
                            showCompany: res.data.show_company === 1,
                            allowSave: res.data.allow_save === 1,
                            allowReshare: res.data.allow_reshare === 1,
                            expiryIndex: res.data.expiry_index || 0
                        };
                    }
                });
            },
            // 上传分享图片
            uploadShareImage() {
                let _this = this;
                uni.chooseImage({
                    count: 1,
                    sizeType: ['compressed'],
                    sourceType: ['album', 'camera'],
                    success: function(res) {
                        const tempFilePath = res.tempFilePaths[0];
                        _this._uploadFile('upload/image', tempFilePath, function(res) {
                            if (res.data.url) {
                                _this.shareConfig.image = res.data.url;
                            }
                        });
                    }
                });
            },
            // 获取有效期文本
            getExpiryText() {
                const expiryOptions = ['永久有效', '7天', '30天', '90天'];
                return expiryOptions[this.shareConfig.expiryIndex] || expiryOptions[0];
            },
            // 有效期选择变化
            onExpiryChange(e) {
                this.shareConfig.expiryIndex = e.detail.value;
            },
            // 保存配置
            saveConfig() {
                let _this = this;
                // 简单验证
                if (!_this.shareConfig.title) {
                    uni.showToast({ title: '请输入分享标题', icon: 'none' });
                    return;
                }
                const params = {
                    business_card_id: _this.currentCardId,
                    title: _this.shareConfig.title,
                    desc: _this.shareConfig.desc,
                    image: _this.shareConfig.image,
                    style: _this.shareConfig.style,
                    show_contact: _this.shareConfig.showContact ? 1 : 0,
                    show_company: _this.shareConfig.showCompany ? 1 : 0,
                    allow_save: _this.shareConfig.allowSave ? 1 : 0,
                    allow_reshare: _this.shareConfig.allowReshare ? 1 : 0,
                    expiry_index: _this.shareConfig.expiryIndex
                };
                _this._post('plus.business/business/saveShareConfig', params, function(res) {
                    if (res.code === 0) {
                        uni.showToast({ title: '保存成功' });
                        uni.navigateBack();
                    }
                });
            }
        }
    };
</script>
<style lang="scss">
    .content {
        padding: 20rpx;
    }
    .setting-section {
        margin-bottom: 30rpx;
        .section-title {
            font-size: 32rpx;
            font-weight: bold;
            color: #333;
            margin-bottom: 20rpx;
        }
        .setting-card {
            background: #fff;
            border-radius: 20rpx;
            padding: 0 30rpx;
            .setting-item {
                display: flex;
                align-items: center;
                padding: 28rpx 0;
                border-bottom: 1rpx solid #f0f0f0;
                &:last-child {
                    border-bottom: none;
                }
                .setting-label {
                    flex: 1;
                    font-size: 30rpx;
                    color: #333;
                }
                .setting-control {
                    flex: 2;
                    text-align: right;
                    .input,
                    .textarea {
                        border: 1rpx solid #e0e0e0;
                        border-radius: 10rpx;
                        padding: 15rpx;
                        font-size: 28rpx;
                        color: #666;
                        ext-align: left;
                        box-sizing: border-box;
                    }
                    .textarea {
                        height: 150rpx;
                        resize: none;
                    }
                    .image-upload {
                        display: inline-block;
                        .uploaded-image {
                            width: 200rpx;
                            height: 200rpx;
                            border-radius: 10rpx;
                        }
                        .upload-placeholder {
                            width: 200rpx;
                            height: 200rpx;
                            border: 2rpx dashed #e0e0e0;
                            border-radius: 10rpx;
                            display: flex;
                            flex-direction: column;
                            align-items: center;
                            justify-content: center;
                            color: #999;
                            font-size: 26rpx;
                            .icon {
                                font-size: 60rpx;
                                margin-bottom: 10rpx;
                            }
                        }
                    }
                    .image-hint {
                        font-size: 24rpx;
                        color: #999;
                        margin-top: 10rpx;
                        text-align: left;
                    }
                    .card-style-list {
                        display: flex;
                        justify-content: space-between;
                        flex-wrap: wrap;
                        .card-style-item {
                            position: relative;
                            width: 220rpx;
                            height: 320rpx;
                            border: 2rpx solid #e0e0e0;
                            border-radius: 10rpx;
                            overflow: hidden;
                            &.active {
                                border-color: #37bde6;
                            }
                            image {
                                width: 100%;
                                height: 100%;
                            }
                            .check-icon {
                                position: absolute;
                                top: 10rpx;
                                right: 10rpx;
                                width: 36rpx;
                                height: 36rpx;
                                background: #37bde6;
                                border-radius: 50%;
                                &::after {
                                    content: '';
                                    position: absolute;
                                    top: 50%;
                                    left: 50%;
                                    transform: translate(-50%, -50%) rotate(45deg);
                                    width: 12rpx;
                                    height: 20rpx;
                                    border-right: 4rpx solid #fff;
                                    border-bottom: 4rpx solid #fff;
                                }
                            }
                        }
                    }
                    .picker {
                        font-size: 28rpx;
                        color: #666;
                        padding: 10rpx 0;
                    }
                }
            }
        }
    }
    .save-btn-container {
        padding: 30rpx 0;
        .save-btn {
            width: 100%;
            height: 90rpx;
            background: #37bde6;
            color: #fff;
            font-size: 32rpx;
            border-radius: 45rpx;
            line-height: 90rpx;
        }
    }
</style>
mobile/pages/plus/business/visitors.vue
New file
@@ -0,0 +1,288 @@
<template>
    <view>
        <header-bar title="访客记录" :isBack="true" @click="back"></header-bar>
        <view class="content">
            <!-- 搜索栏 -->
            <view class="search-bar">
                <view class="search-input">
                    <text class="icon iconfont icon-sousuo"></text>
                    <input type="text" v-model="search" placeholder="搜索访客姓名" @confirm="searchVisitor" />
                </view>
            </view>
        <!-- 访客列表 -->
        <scroll-view scroll-y="true" class="visitor-list" @scrolltolower="loadMore">
            <view v-if="visitors.length > 0">
                <view class="visitor-item" v-for="(visitor, index) in visitors" :key="index">
                    <image class="visitor-avatar" :src="visitor.avatar || '/static/default.png'" mode="aspectFill"></image>
                    <view class="visitor-info">
                        <view class="visitor-header">
                            <view class="visitor-name">{{visitor.user_name || '未知访客'}}</view>
                            <view class="visitor-time">{{formatTime(visitor.visit_time)}}</view>
                        </view>
                        <view v-if="visitor.company_name" class="visitor-company">{{visitor.company_name}}</view>
                        <view v-if="visitor.position" class="visitor-position">{{visitor.position}}</view>
                        <view class="visitor-action">
                            <view class="action-btn" @click="viewCard(visitor.business_card_id)">
                                <text class="icon iconfont icon-card"></text>
                                <text>查看名片</text>
                            </view>
                            <view class="action-btn" @click="contactVisitor(visitor)">
                                <text class="icon iconfont icon-message"></text>
                                <text>联系访客</text>
                            </view>
                        </view>
                    </view>
                </view>
            </view>
            <!-- 无数据提示 -->
            <view v-else-if="!loading" class="no-data">
                <text class="icon iconfont icon-wushuju"></text>
                <text class="text">暂无访客记录</text>
            </view>
            <!-- 加载中 -->
            <view v-if="loading && visitors.length > 0" class="loading-more">
                <text>加载中...</text>
            </view>
            <!-- 无更多数据 -->
            <view v-if="!hasMore && visitors.length > 0" class="no-more">
                <text>没有更多了</text>
            </view>
        </scroll-view>
    </view>
</template>
<script>
    export default {
        data() {
            return {
                visitors: [],
                loading: false,
                page: 1,
                list_rows: 10,
                hasMore: true,
                search: ''
            };
        },
        onLoad() {
            this.getVisitors();
        },
        methods: {
            back() {
                uni.navigateBack();
            },
            // 获取访客列表
            getVisitors() {
                let _this = this;
                _this.loading = true;
                const params = {
                    page: _this.page,
                    list_rows: _this.list_rows
                };
                if (_this.search) {
                    params.search = _this.search;
                }
                _this._post('plus.business/business/getVisitors', params, function(res) {
                    _this.loading = false;
                    if (_this.page === 1) {
                        _this.visitors = res.data.list;
                    } else {
                        _this.visitors = _this.visitors.concat(res.data.list);
                    }
                    // 判断是否还有更多数据
                    _this.hasMore = _this.visitors.length < res.data.total;
                });
            },
            // 搜索访客
            searchVisitor() {
                this.page = 1;
                this.getVisitors();
            },
            // 加载更多
            loadMore() {
                if (!this.loading && this.hasMore) {
                    this.page++;
                    this.getVisitors();
                }
            },
            // 查看访客名片
            viewCard(business_card_id) {
                this.gotoPage(`/pages/plus/business/detail?business_card_id=${business_card_id}`);
            },
            // 联系访客
            contactVisitor(visitor) {
                // 这里可以根据系统功能扩展,比如发送消息、拨打电话等
                uni.showActionSheet({
                    itemList: ['发送消息', '拨打电话'],
                    success: (res) => {
                        if (res.tapIndex === 0) {
                            // 发送消息
                            this.showError('消息功能暂未开放');
                        } else if (res.tapIndex === 1) {
                            // 拨打电话
                            if (visitor.phone) {
                                uni.makePhoneCall({
                                    phoneNumber: visitor.phone
                                });
                            } else {
                                this.showError('暂无联系电话');
                            }
                        }
                    }
                });
            },
            // 格式化时间
            formatTime(time) {
                if (!time) return '';
                const date = new Date(time);
                const now = new Date();
                const diff = now - date;
                const days = Math.floor(diff / (1000 * 60 * 60 * 24));
                if (days === 0) {
                    return '今天 ' + date.getHours() + ':' + (date.getMinutes() < 10 ? '0' : '') + date.getMinutes();
                } else if (days === 1) {
                    return '昨天 ' + date.getHours() + ':' + (date.getMinutes() < 10 ? '0' : '') + date.getMinutes();
                } else if (days < 7) {
                    return days + '天前';
                } else {
                    return date.getFullYear() + '-' + (date.getMonth() + 1) + '-' + date.getDate();
                }
            }
        }
    };
</script>
<style lang="scss">
    .content {
        padding: 20rpx;
    }
    .search-bar {
        background: #fff;
        padding: 20rpx;
        border-radius: 10rpx;
        margin-bottom: 20rpx;
        .search-input {
            display: flex;
            align-items: center;
            background: #f5f5f5;
            border-radius: 60rpx;
            padding: 0 30rpx;
            .icon {
                font-size: 32rpx;
                color: #999;
                margin-right: 20rpx;
            }
            input {
                flex: 1;
                height: 80rpx;
                font-size: 28rpx;
                color: #333;
            }
        }
    }
    .visitor-list {
        height: calc(100vh - 200rpx);
        .visitor-item {
            background: #fff;
            border-radius: 20rpx;
            padding: 30rpx;
            margin-bottom: 20rpx;
            display: flex;
            .visitor-avatar {
                width: 120rpx;
                height: 120rpx;
                border-radius: 50%;
                background: #f5f5f5;
            }
            .visitor-info {
                flex: 1;
                margin-left: 30rpx;
                .visitor-header {
                    display: flex;
                    justify-content: space-between;
                    align-items: center;
                    margin-bottom: 10rpx;
                    .visitor-name {
                        font-size: 34rpx;
                        font-weight: bold;
                        color: #333;
                    }
                    .visitor-time {
                        font-size: 24rpx;
                        color: #999;
                    }
                }
                .visitor-company,
                .visitor-position {
                    font-size: 28rpx;
                    color: #666;
                    margin-bottom: 5rpx;
                }
                .visitor-action {
                    display: flex;
                    margin-top: 20rpx;
                    .action-btn {
                        display: flex;
                        align-items: center;
                        margin-right: 40rpx;
                        .icon {
                            font-size: 28rpx;
                            color: #37bde6;
                            margin-right: 8rpx;
                        }
                        text {
                            font-size: 26rpx;
                            color: #37bde6;
                        }
                    }
                }
            }
        }
        .no-data {
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
            height: 400rpx;
            .icon {
                font-size: 120rpx;
                color: #ccc;
                margin-bottom: 30rpx;
            }
            .text {
                font-size: 30rpx;
                color: #999;
            }
        }
        .loading-more,
        .no-more {
            text-align: center;
            padding: 30rpx 0;
            font-size: 28rpx;
            color: #999;
        }
    }
</style>
shop_vue/src/api/business.js
New file
@@ -0,0 +1,55 @@
import request from '@/utils/request';
let BusinessApi = {
    /*模板列表*/
    templateList(data, errorback) {
        return request._post('/shop/plus.business.template/index', data, errorback);
    },
    /*模板详情*/
    templateDetail(data, errorback) {
        return request._get('/shop/plus.business.template/edit', data, errorback);
    },
    /*添加模板*/
    templateAdd(data, errorback) {
        return request._post('/shop/plus.business.template/add', data, errorback);
    },
    /*保存模板*/
    templateSave(data, errorback) {
        return request._post('/shop/plus.business.template/add', data, errorback);
    },
    /*修改模板*/
    templateEdit(data, errorback) {
        return request._post('/shop/plus.business.template/edit', data, errorback);
    },
    /*删除模板*/
    templateDelete(data, errorback) {
        return request._post('/shop/plus.business.template/delete', data, errorback);
    },
    /*设置默认模板*/
    templateDefault(data, errorback) {
        return request._get('/shop/plus.business.template/add', data, errorback);
    },
    /*名片管理列表*/
    businessList(data, errorback) {
        return request._post('/shop/plus.business.business/index', data, errorback);
    },
    /*名片详情*/
    businessDetail(data, errorback) {
        return request._get('/shop/plus.business.business/edit', data, errorback);
    },
    /*添加名片*/
    businessAdd(data, errorback) {
        return request._post('/shop/plus.business.business/add', data, errorback);
    },
    /*修改名片*/
    businessEdit(data, errorback) {
        return request._post('/shop/plus.business.business/edit', data, errorback);
    },
    /*删除名片*/
    businessDelete(data, errorback) {
        return request._post('/shop/plus.business.business/delete', data, errorback);
    }
};
export default BusinessApi;
shop_vue/src/views/plus/business/business/index.vue
New file
@@ -0,0 +1,7 @@
<template>
</template>
<style scoped>
</style>
shop_vue/src/views/plus/business/grade/index.vue
New file
@@ -0,0 +1,278 @@
<template>
  <!--
      作者:系统
      时间:2025-10-28
      描述:名片等级管理
  -->
  <div class="common-seach-wrap">
    <div class="search-left">
      <el-button type="primary" @click="addGrade" v-auth="'/plus/business/grade/add'">添加等级</el-button>
    </div>
    <div class="table-container">
      <el-table v-loading="loading" :data="listData" border>
        <el-table-column prop="grade_id" label="等级ID" width="80" align="center"></el-table-column>
        <el-table-column prop="name" label="等级名称" align="center"></el-table-column>
        <el-table-column prop="price" label="查看联系方式价格" align="center">
          <template slot-scope="scope">
            {{ scope.row.price>0 ? '¥' + scope.row.price : '免费' }}
          </template>
        </el-table-column>
        <el-table-column prop="weight" label="权重" align="center">
           <!--<template slot-scope="scope">
            <el-input-number
              v-model="scope.row.weight"
              :min="0"
              size="mini"
              @change="handleWeightChange(scope.row)"
              v-auth="'/plus/business/grade/edit'"
            ></el-input-number>
          </template>-->
        </el-table-column>
        <el-table-column prop="create_time" label="创建时间" width="180" align="center">
        </el-table-column>
        <el-table-column prop="update_time" label="更新时间" width="180" align="center">
        </el-table-column>
        <el-table-column label="操作" width="180" align="center">
         <template slot-scope="scope">
            <el-button
              type="text"
              @click="editGrade(scope.row)"
              v-auth="'/plus/business/grade/edit'"
            >编辑</el-button>
            <el-button
              type="text"
              @click="deleteGrade(scope.row)"
              v-auth="'/plus/business/grade/delete'"
            >删除</el-button>
          </template>
        </el-table-column>
      </el-table>
    </div>
    <!-- 添加/编辑等级对话框 -->
    <el-dialog
      :title="dialogTitle"
      :visible.sync="dialogVisible"
      width="500px"
      @close="handleClose"
    >
      <el-form :model="formData" :rules="rules" ref="formData" label-width="150px">
        <el-form-item label="等级名称" prop="name">
          <el-input v-model="formData.name" placeholder="请输入等级名称"></el-input>
        </el-form-item>
        <el-form-item label="查看联系方式价格" prop="price">
          <el-input
            v-model.number="formData.price"
            placeholder="请输入价格,0表示免费"
            type="number"
            :min="0"
            :step="0.01"
          ></el-input>
        </el-form-item>
        <el-form-item label="权重" prop="weight">
          <el-input-number
            v-model="formData.weight"
            :min="0"
            placeholder="权重越小,排序越靠前"
          ></el-input-number>
        </el-form-item>
      </el-form>
      <div slot="footer" class="dialog-footer">
        <el-button @click="dialogVisible = false">取消</el-button>
        <el-button type="primary" @click="submitForm">确定</el-button>
      </div>
    </el-dialog>
  </div>
</template>
<script>
import PlusApi from '@/api/plus.js';
export default {
  data() {
    return {
      loading: false,
      listData: [],
      dialogVisible: false,
      dialogTitle: '',
      formData: {
        grade_id: '',
        name: '',
        price: 0.00,
        weight: 100
      },
      rules: {
        name: [
          { required: true, message: '请输入等级名称', trigger: 'blur' },
          { min: 1, max: 20, message: '等级名称长度在 1 到 20 个字符', trigger: 'blur' }
        ],
        price: [
          { type: 'number', min: 0, message: '价格不能小于0', trigger: 'blur' }
        ],
        weight: [
          { type: 'number', required: true, message: '请输入权重', trigger: 'blur' },
          { type: 'number', min: 0, message: '权重不能小于0', trigger: 'blur' }
        ]
      }
    };
  },
  mounted() {
    this.loadData();
  },
  methods: {
    // 加载数据
    loadData() {
      this.loading = true;
      PlusApi.gradeIndex().then(res => {
        this.loading = false;
        this.listData = res.data.list || [];
        console.log(this.listData);
      }).catch(() => {
        this.loading = false;
        this.$message.error('获取数据失败');
      });
    },
    // 添加等级
    addGrade() {
      this.dialogTitle = '添加等级';
      this.formData = {
        grade_id: '',
        name: '',
        price: 0.00,
        weight: 100
      };
      this.dialogVisible = true;
    },
    // 编辑等级
    editGrade(row) {
      this.dialogTitle = '编辑等级';
      this.formData = {
        grade_id: row.grade_id,
        name: row.name,
        price: parseFloat(row.price) || 0.00,
        weight: parseInt(row.weight) || 100
      };
      this.dialogVisible = true;
    },
    // 关闭对话框
    handleClose() {
      this.$refs.formData.resetFields();
    },
    // 提交表单
    submitForm() {
      this.$refs.formData.validate((valid) => {
        if (valid) {
          const params = { ...this.formData };
          if (params.grade_id) {
            // 编辑
            PlusApi.gradeEdit(params).then(res => {
              if (res.code === 1) {
                this.$message.success('编辑成功');
                this.dialogVisible = false;
                this.loadData();
              } else {
                this.$message.error(res.msg || '编辑失败');
              }
            }).catch(() => {
              this.$message.error('编辑失败');
            });
          } else {
            // 添加
            PlusApi.gradeAdd(params).then(res => {
              if (res.code === 1) {
                this.$message.success('添加成功');
                this.dialogVisible = false;
                this.loadData();
              } else {
                this.$message.error(res.msg || '添加失败');
              }
            }).catch(() => {
              this.$message.error('添加失败');
            });
          }
        }
      });
    },
    // 删除等级
    deleteGrade(row) {
      this.$confirm('确定要删除这个等级吗?', '提示', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning'
      }).then(() => {
        PlusApi.gradeDelete({ grade_id: row.grade_id }).then(res => {
          if (res.code === 1) {
            this.$message.success('删除成功');
            this.loadData();
          } else {
            this.$message.error(res.msg || '删除失败');
          }
        }).catch(() => {
          this.$message.error('删除失败');
        });
      }).catch(() => {});
    },
    // 处理权重变化
    handleWeightChange(row) {
      const params = {
        grade_id: row.grade_id,
        weight: row.weight,
        name: row.name,
        price: row.price
      };
      PlusApi.gradeEdit(params).then(res => {
        if (res.code !== 1) {
          this.$message.error(res.msg || '更新权重失败');
          this.loadData(); // 重新加载数据
        }
      }).catch(() => {
        this.$message.error('更新权重失败');
        this.loadData();
      });
    },
    // 格式化日期
    formatDate(timestamp) {
      if (!timestamp) return '';
      const date = new Date(timestamp * 1000);
      const year = date.getFullYear();
      const month = (date.getMonth() + 1).toString().padStart(2, '0');
      const day = date.getDate().toString().padStart(2, '0');
      const hours = date.getHours().toString().padStart(2, '0');
      const minutes = date.getMinutes().toString().padStart(2, '0');
      const seconds = date.getSeconds().toString().padStart(2, '0');
      return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
    },
  }
};
</script>
<style scoped>
.common-seach-wrap {
  padding: 20px;
  background-color: #fff;
  min-height: calc(100vh - 60px);
}
.search-left {
  margin-bottom: 20px;
}
.table-container {
  margin-top: 10px;
}
.dialog-footer {
  text-align: right;
}
</style>
shop_vue/src/views/plus/business/index.vue
New file
@@ -0,0 +1,135 @@
<template>
  <!--
          作者:luoyiming
          时间:2019-06-04
          描述:插件中心-分销
      -->
  <div class="common-seach-wrap">
    <!--名片列表-->
    <Business v-if="activeName == 'business'"></Business>
    <!--名片模板记录-->
    <Template v-if="activeName == 'template'"></Template>
    <!--名片等级管理-->
    <Grade v-if="activeName == 'grade'"></Grade>
    <!--行业管理-->
    <Industry v-if="activeName == 'industry'"></Industry>
  </div>
</template>
<script>
import bus from '@/utils/eventBus.js';
import PlusApi from '@/api/plus.js';
import Business from './business/index.vue';
import Template from './template/index.vue';
import Grade from './grade/index.vue';
import Industry from './industry/index.vue';
export default {
  components: {
    Business,
    Template,
    Grade,
    Industry
  },
  data() {
    return {
      formInline: {
        nick_name: ''
      },
      /*参数*/
      param: {},
      /*当前选中*/
      activeName: 'business',
      /*切换数组*/
      sourceList: [
          {
            key: 'industry',
            value: '行业管理',
            path: '/plus/business/industry/index',
          },
        {
          key: 'business',
          value: '名片列表',
          path:'/plus/business/business/index'
        },
        {
          key: 'template',
          value: '名片模板记录',
          path:'/plus/business/template/index'
        },
        {
          key: 'grade',
          value: '名片等级管理',
          path:'/plus/business/grade/index'
        }
      ],
      /*权限筛选后的数据*/
      tabList:[],
      /*判断third是否有参数*/
      is_third_param: false
    };
  },
  watch:{
    //监听路由
    $route(to, from) {
      this.init();
    }
  },
  created() {
    this.init();
  },
  beforeDestroy() {
    //发送类别切换
    bus.$emit('tabData', { active: null, tab_type:'business',list: [] });
    bus.$off('activeValue');
  },
  methods: {
    /*初始化方法*/
    init(){
      this.tabList=this.authFilter();
      if(this.tabList.length>0){
        this.activeName=this.tabList[0].key;
      }
      if (this.$route.query.type != null) {
        this.activeName = this.$route.query.type;
      }
      /*监听传插件的值*/
      bus.$on('activeValue', res => {
        if (this.is_third_param) {
          this.param.user_id = '';
          this.is_third_param = false;
        }
        this.activeName = res;
      });
      //发送类别切换
      let params = {
        active: this.activeName,
        list: this.tabList,
        tab_type:'business'
      };
      bus.$emit('tabData', params);
    },
    /*权限过滤*/
    authFilter(){
      let list=[];
      for(let i=0;i<this.sourceList.length;i++){
        let item=this.sourceList[i];
        if(this.$filter.isAuth(item.path)){
          list.push(item);
        }
      }
      return list;
    }
  }
};
</script>
shop_vue/src/views/plus/business/industry/Add.vue
New file
@@ -0,0 +1,104 @@
<template>
  <!--
        描述:行业管理-添加
    -->
  <el-dialog title="添加行业" :visible.sync="dialogVisible" @close="dialogFormVisible" :close-on-click-modal="false"
    :close-on-press-escape="false">
    <el-form size="small" :model="form" :rules="formRules" ref="form">
      <el-form-item label="所属行业" :label-width="formLabelWidth">
        <el-select v-model="form.parent_id">
          <el-option label="顶级行业" value="0"></el-option>
          <el-option :value="cat.industry_id" :label="cat.name" :key="cat.industry_id" v-for="cat in addform.industryOptions"></el-option>
        </el-select>
      </el-form-item>
      <el-form-item label="行业名称" prop="name" :label-width="formLabelWidth">
        <el-input v-model="form.name" autocomplete="off"></el-input>
      </el-form-item>
      <el-form-item label="排序" prop="sort" :label-width="formLabelWidth">
        <el-input v-model.number="form.sort" autocomplete="off"></el-input>
      </el-form-item>
    </el-form>
    <div slot="footer" class="dialog-footer">
      <el-button @click="dialogFormVisible">取 消</el-button>
      <el-button type="primary" @click="addIndustry" :loading="loading">确 定</el-button>
    </div>
  </el-dialog>
</template>
<script>
  import PlusApi from '@/api/plus';
  export default {
    data() {
      return {
        form: {
          parent_id: '0',
          name: '',
          sort: 100
        },
        formRules: {
          name: [{
            required: true,
            message: '请输入行业名称',
            trigger: 'blur'
          }],
          sort: [{
            required: true,
            message: '排序不能为空'
          }, {
            type: 'number',
            message: '排序必须为数字'
          }]
        },
        /*左边长度*/
        formLabelWidth: '120px',
        /*是否显示*/
        dialogVisible: false,
        loading: false
      };
    },
    props: ['open_add', 'addform'],
    created() {
      this.dialogVisible = this.open_add;
    },
    methods: {
      /*添加行业*/
      addIndustry() {
        let self = this;
        let params = self.form;
        self.$refs.form.validate((valid) => {
          if (valid) {
            self.loading = true;
            PlusApi.addIndustry(params).then(data => {
              self.loading = false;
              self.$message({
                message: '添加成功',
                type: 'success'
              });
              self.dialogFormVisible(true);
            }).catch(error => {
              self.loading = false;
            });
          }
        });
      },
      /*关闭弹窗*/
      dialogFormVisible(e) {
        if (e) {
          this.$emit('closeDialog', {
            type: 'success',
            openDialog: false
          })
        } else {
          this.$emit('closeDialog', {
            type: 'error',
            openDialog: false
          })
        }
      }
    }
  };
</script>
<style>
</style>
shop_vue/src/views/plus/business/industry/Edit.vue
New file
@@ -0,0 +1,108 @@
<template>
  <!--
        描述:行业管理-修改
    -->
  <el-dialog title="修改行业" :visible.sync="dialogVisible" @close="dialogFormVisible" :close-on-click-modal="false"
    :close-on-press-escape="false">
    <el-form size="small" :model="form" :rules="formRules" ref="form">
      <el-form-item label="所属行业" :label-width="formLabelWidth">
        <el-select v-model="form.parent_id">
          <el-option label="顶级行业" :value="0"></el-option>
          <el-option :value="cat.industry_id" :label="cat.name" :key="cat.industry_id" v-for="cat in editform.industryOptions"></el-option>
        </el-select>
      </el-form-item>
      <el-form-item label="行业名称" prop="name" :label-width="formLabelWidth">
        <el-input v-model="form.name" autocomplete="off"></el-input>
      </el-form-item>
      <el-form-item label="排序" prop="sort" :label-width="formLabelWidth">
        <el-input v-model.number="form.sort" autocomplete="off"></el-input>
      </el-form-item>
    </el-form>
    <div slot="footer" class="dialog-footer">
      <el-button @click="dialogFormVisible">取 消</el-button>
      <el-button type="primary" @click="editIndustry" :loading="loading">确 定</el-button>
    </div>
  </el-dialog>
</template>
<script>
  import PlusApi from '@/api/plus';
  export default {
    data() {
      return {
        form: {
          industry_id: 0,
          parent_id: 0,
          name: '',
          sort: ''
        },
        formRules: {
          name: [{
            required: true,
            message: '请输入行业名称',
            trigger: 'blur'
          }],
          sort: [{
            required: true,
            message: '排序不能为空'
          }, {
            type: 'number',
            message: '排序必须为数字'
          }]
        },
        /*左边长度*/
        formLabelWidth: '120px',
        /*是否显示*/
        dialogVisible: false,
        loading: false
      };
    },
    props: ['open_edit', 'editform'],
    created() {
      this.dialogVisible = this.open_edit;
      this.form.industry_id = this.editform.model.industry_id;
      this.form.parent_id = this.editform.model.parent_id;
      this.form.name = this.editform.model.name;
      this.form.sort = this.editform.model.sort;
    },
    methods: {
      /*修改行业*/
      editIndustry() {
        let self = this;
        let params = self.form;
        self.$refs.form.validate((valid) => {
          if (valid) {
            self.loading = true;
            PlusApi.editIndustry(params, true).then(data => {
              self.loading = false;
              self.$message({
                message: '修改成功',
                type: 'success'
              });
              self.dialogFormVisible(true);
            }).catch(error => {
              self.loading = false;
            });
          }
        });
      },
      /*关闭弹窗*/
      dialogFormVisible(e) {
        if (e) {
          this.$emit('closeDialog', {
            type: 'success',
            openDialog: false
          })
        } else {
          this.$emit('closeDialog', {
            type: 'error',
            openDialog: false
          })
        }
      }
    }
  };
</script>
<style>
</style>
shop_vue/src/views/plus/business/industry/index.vue
New file
@@ -0,0 +1,179 @@
<template>
  <!--
      作者:wangxw
      时间:2023-10-26
      描述:行业管理
  -->
  <div class="product">
    <!--添加行业-->
    <div class="common-level-rail">
      <el-button size="small" type="primary" @click="addClick" icon="el-icon-plus" v-auth="'/plus/business/industry/add'">添加行业</el-button>
    </div>
    <!--内容-->
    <div class="product-content">
      <div class="table-wrap">
        <el-table size="small" :data="tableData" row-key="industry_id" default-expand-all :tree-props="{children: 'child'}"
          style="width: 100%" v-loading="loading">
          <el-table-column prop="industry_id" label="行业ID" width="80" align="center"></el-table-column>
          <el-table-column prop="name" label="行业名称" width="180"></el-table-column>
          <el-table-column prop="sort" label="排序"></el-table-column>
          <el-table-column prop="create_time" label="创建时间"></el-table-column>
          <el-table-column prop="status" label="状态">
            <template slot-scope="scope">
              <el-checkbox v-model="scope.row.status" :checked="scope.row.status" @change="checked => statusChange(checked, scope.row)">启用</el-checkbox>
            </template>
          </el-table-column>
          <el-table-column fixed="right" label="操作" width="100">
            <template slot-scope="scope">
              <el-button @click="editClick(scope.row)" type="text" size="small" v-auth="'/plus/business/industry/edit'">编辑</el-button>
              <el-button @click="deleteClick(scope.row)" type="text" size="small" v-auth="'/plus/business/industry/delete'">删除</el-button>
            </template>
          </el-table-column>
        </el-table>
      </div>
    </div>
    <!--添加-->
    <Add v-if="open_add" :open_add="open_add" :addform="industryModel" @closeDialog="closeDialogFunc($event, 'add')"></Add>
    <!--修改-->
    <Edit v-if="open_edit" :open_edit="open_edit" :editform="industryModel" @closeDialog="closeDialogFunc($event, 'edit')"></Edit>
  </div>
</template>
<script>
import PlusApi from '@/api/plus';
import Add from './Add.vue';
import Edit from './Edit.vue';
export default {
  components: {
    Add,
    Edit
  },
  data() {
    return {
      /*是否加载完成*/
      loading: true,
      /*列表数据*/
      tableData: [],
      /*是否打开添加弹窗*/
      open_add: false,
      /*是否打开编辑弹窗*/
      open_edit: false,
      /*当前编辑的对象*/
      industryModel: {
        industryOptions: [],
        model: {}
      }
    };
  },
  created() {
    /*获取列表*/
    this.getData();
  },
  methods: {
    /*获取列表*/
    getData() {
      let self = this;
      PlusApi.getIndustryList({}, true)
        .then(data => {
          self.loading = false;
          self.tableData = data.data.list.tree;
          self.industryModel.industryOptions = data.data.list.tree;
        })
        .catch(error => {
          self.loading = false;
        });
    },
    /*打开添加*/
    addClick() {
      this.open_add = true;
    },
    /*打开编辑*/
    editClick(item) {
      this.industryModel.model = item;
      this.open_edit = true;
    },
    /*关闭弹窗*/
    closeDialogFunc(e, f) {
      if (f == 'add') {
        this.open_add = e.openDialog;
        if (e.type == 'success') {
          this.getData();
        }
      }
      if (f == 'edit') {
        this.open_edit = e.openDialog;
        if (e.type == 'success') {
          this.getData();
        }
      }
    },
    /*删除行业*/
    deleteClick(row) {
      let self = this;
      self.$confirm('删除后不可恢复,确认删除该记录吗?', '提示', {
        type: 'warning'
      }).then(() => {
        PlusApi.deleteIndustry({
          industry_id: row.industry_id
        }).then(data => {
          self.$message({
            message: '删除成功',
            type: 'success'
          });
          self.getData();
        });
      });
    },
    /*启用/禁用*/
    statusChange: function(checked, row) {
      let self = this;
      let status = checked ? 1 : 0;
      self.loading = true;
      let params = {
        industry_id: row.industry_id,
        parent_id: row.parent_id,
        name: row.name,
        sort: row.sort,
        status: status
      };
      PlusApi.editIndustry(
        params,
        true
      )
        .then(data => {
          self.loading = false;
          row.status = checked;
        })
        .catch(error => {
          self.loading = false;
          row.status = checked ? 0 : 1;
        });
    },
  }
};
</script>
<style scoped>
.product {
  padding: 20px;
}
.common-level-rail {
  margin-bottom: 20px;
}
.product-content {
  clear: both;
}
.table-wrap {
  background: #fff;
  padding: 20px;
  border-radius: 4px;
}
</style>
shop_vue/src/views/plus/business/template/add.vue
New file
@@ -0,0 +1,993 @@
<template>
  <!--
    作者:系统自动生成
    时间:当前日期
    描述:插件中心-名片模板-添加模板
  -->
  <div class="user" v-loading="loading">
    <div class="common-form">名片模板添加</div>
    <div class="poster-box d-s-s">
      <div class="left-box">
        <div v-if="form.backdrop" class="img"><img v-img-url="form.backdrop.src" /></div>
        <div class="userinfo">
          <!-- 头像 -->
          <div v-if="form.avatar.display == 1"
               class="photo pa"
               v-drag="{type:'avatar',obj:this}"
               :class="{ radius: form.avatar.style == 'circle' }"
               :style="'width:' + form.avatar.width + 'px;height:' + form.avatar.width + 'px;top:' + form.avatar.top + 'px;left:' + form.avatar.left + 'px;background-color:#f0f0f0;display:flex;align-items:center;justify-content:center;color:#999'">
            <span>头像</span>
          </div>
          <!-- Logo -->
          <div v-if="form.logo.display == 1"
               class="logo pa"
               v-drag="{type:'logo',obj:this}"
               :class="{ radius: form.logo.style == 'circle' }"
               :style="'width:' + form.logo.width + 'px;height:' + form.logo.height + 'px;top:' + form.logo.top + 'px;left:' + form.logo.left + 'px;'">
            <img v-img-url="form.logo.src" alt="" />
          </div>
          <!-- 姓名 -->
          <div class="name pa"
               v-drag="{type:'name',obj:this}"
               :style="'font-size:' + form.name.fontSize + 'px;color:' + form.name.color + ';top:' + form.name.top + 'px;left:' + form.name.left + 'px;'">
            这里是姓名
          </div>
          <!-- 手机 -->
          <div class="mobile pa"
               v-drag="{type:'mobile',obj:this}"
               :style="'font-size:' + form.mobile.fontSize + 'px;color:' + form.mobile.color + ';top:' + form.mobile.top + 'px;left:' + form.mobile.left + 'px;'">
            手机:134xxxxxxxx
          </div>
          <!-- 公司 -->
          <div v-for="(item, index) in form.unit" :key="'unit' + index"
               :class="'unit' + index + ' pa'"
               v-drag="{type:'unit', index:index, obj:this}"
               :style="'font-size:' + form.unit[index].fontSize + 'px;color:' + form.unit[index].color + ';top:' + form.unit[index].top + 'px;left:' + form.unit[index].left + 'px;'">
            这是公司{{index+1}}
          </div>
          <!-- 职位 -->
          <div v-for="(item, index) in form.duties" :key="'duties' + index"
               :class="'duties' + index + ' pa'"
               v-drag="{type:'duties', index:index, obj:this}"
               :style="'font-size:' + form.duties[index].fontSize + 'px;color:' + form.duties[index].color + ';top:' + form.duties[index].top + 'px;left:' + form.duties[index].left + 'px;'">
            这是职位{{index+1}}
          </div>
          <!-- 地址 -->
          <div v-for="(item, index) in form.address" :key="'address' + index"
               :class="'address' + index + ' pa'"
               v-drag="{type:'address', index:index, obj:this}"
               :style="'font-size:' + form.address[index].fontSize + 'px;color:' + form.address[index].color + ';top:' + form.address[index].top + 'px;left:' + form.address[index].left + 'px;'">
            地址:广西壮族自治区南宁市江南区壮锦大道八桂绿城·龙湖御景-A栋-2单元{{index+1}}号
          </div>
          <!-- 微信 -->
          <div class="wechat pa"
               v-drag="{type:'wechat',obj:this}"
               :style="'font-size:' + form.wechat.fontSize + 'px;color:' + form.wechat.color + ';top:' + form.wechat.top + 'px;left:' + form.wechat.left + 'px;'">
            微信:134xxxxxxxx
          </div>
          <!-- 邮箱 -->
          <div class="mailbox pa"
               v-drag="{type:'mailbox',obj:this}"
               :style="'font-size:' + form.mailbox.fontSize + 'px;color:' + form.mailbox.color + ';top:' + form.mailbox.top + 'px;left:' + form.mailbox.left + 'px;'">
            邮箱:134xxxxxxxx@.xxx.com
          </div>
          <!-- 电话 -->
          <div class="phone pa"
               v-drag="{type:'phone',obj:this}"
               :style="'font-size:' + form.phone.fontSize + 'px;color:' + form.phone.color + ';top:' + form.phone.top + 'px;left:' + form.phone.left + 'px;'">
            电话:xxx-xxx-xxx
          </div>
          <!-- 自定义图标 -->
          <div v-for="(item, index) in form.iconL" :key="'iconL' + index"
               :class="'iconL' + index + ' pa icon'"
               v-drag="{type:'iconL', index:index, obj:this}"
               :style="'width:' + form.iconL[index].width + 'px;height:' + form.iconL[index].height + 'px;top:' + form.iconL[index].top + 'px;left:' + form.iconL[index].left + 'px;'">
            <img v-img-url="form.iconL[index].src" alt="" />
          </div>
        </div>
      </div>
      <div class="right-box flex-1">
        <el-form size="small" ref="form" :model="form" label-width="150px">
          <!-- 背景图 -->
          <el-form-item label="海报背景图">
            <el-button type="primary" @click="openUpload(1)">上传图片</el-button>
            <div class="tips">尺寸建议:宽750像素 高大于(等于)1200像素</div>
          </el-form-item>
          <!-- 头像设置 -->
          <el-form-item label="是否显示头像">
            <el-radio v-model="form.avatar.display" label="1">显示</el-radio>
            <el-radio v-model="form.avatar.display" label="0">隐藏</el-radio>
          </el-form-item>
          <el-form-item v-if="form.avatar.display == 1" label="头像宽度" prop="avatar.width" :rules="[{ required: true, message: '请输入头像宽度' }]">
            <el-input v-model.number="form.avatar.width" min="30" type="number" class="max-w460"></el-input>
          </el-form-item>
          <el-form-item v-if="form.avatar.display == 1" label="头像样式">
            <el-radio v-model="form.avatar.style" label="square">正方形</el-radio>
            <el-radio v-model="form.avatar.style" label="circle">圆形</el-radio>
          </el-form-item>
          <el-form-item v-if="form.avatar.display == 1" label="头像位置">
            <div class="d-s-r">
              <el-input v-model.number="form.avatar.left" min="0" type="number" class="max-w200" placeholder="左右位置"></el-input>
              <span class="mr-10">x</span>
              <el-input v-model.number="form.avatar.top" min="0" type="number" class="max-w200" placeholder="上下位置"></el-input>
            </div>
          </el-form-item>
          <!-- Logo设置 -->
          <el-form-item label="是否显示Logo">
            <el-radio v-model="form.logo.display" label="1">显示</el-radio>
            <el-radio v-model="form.logo.display" label="0">隐藏</el-radio>
          </el-form-item>
          <el-form-item v-if="form.logo.display == 1" label="Logo图片">
            <el-button type="primary" @click="openUpload(2)">上传图片</el-button>
          </el-form-item>
          <el-form-item v-if="form.logo.display == 1" label="Logo尺寸" prop="logo.width" :rules="[{ required: true, message: '请输入Logo宽度' }]">
            <div class="d-s-r">
              <el-input v-model.number="form.logo.width" min="10" type="number" class="max-w200" placeholder="宽度"></el-input>
              <span class="mr-10">x</span>
              <el-input v-model.number="form.logo.height" min="10" type="number" class="max-w200" placeholder="高度"></el-input>
            </div>
          </el-form-item>
          <el-form-item v-if="form.logo.display == 1" label="Logo样式">
            <el-radio v-model="form.logo.style" label="square">正方形</el-radio>
            <el-radio v-model="form.logo.style" label="circle">圆形</el-radio>
          </el-form-item>
          <el-form-item v-if="form.logo.display == 1" label="Logo位置">
            <div class="d-s-r">
              <el-input v-model.number="form.logo.left" min="0" type="number" class="max-w200" placeholder="左右位置"></el-input>
              <span class="mr-10">x</span>
              <el-input v-model.number="form.logo.top" min="0" type="number" class="max-w200" placeholder="上下位置"></el-input>
            </div>
          </el-form-item>
          <!-- 姓名设置 -->
          <el-form-item label="姓名字体大小" prop="name.fontSize" :rules="[{ required: true, message: '请输入字体大小' }]">
            <el-input v-model.number="form.name.fontSize" min="12" type="number" class="max-w460"></el-input>
          </el-form-item>
          <el-form-item label="姓名字体颜色">
            <el-color-picker v-model="form.name.color"></el-color-picker>
          </el-form-item>
          <el-form-item label="姓名位置">
            <div class="d-s-r">
              <el-input v-model.number="form.name.left" min="0" type="number" class="max-w200" placeholder="左右位置"></el-input>
              <span class="mr-10">x</span>
              <el-input v-model.number="form.name.top" min="0" type="number" class="max-w200" placeholder="上下位置"></el-input>
            </div>
          </el-form-item>
          <!-- 手机设置 -->
          <el-form-item label="手机字体大小" prop="mobile.fontSize" :rules="[{ required: true, message: '请输入字体大小' }]">
            <el-input v-model.number="form.mobile.fontSize" min="12" type="number" class="max-w460"></el-input>
          </el-form-item>
          <el-form-item label="手机字体颜色">
            <el-color-picker v-model="form.mobile.color"></el-color-picker>
          </el-form-item>
          <el-form-item label="手机位置">
            <div class="d-s-r">
              <el-input v-model.number="form.mobile.left" min="0" type="number" class="max-w200" placeholder="左右位置"></el-input>
              <span class="mr-10">x</span>
              <el-input v-model.number="form.mobile.top" min="0" type="number" class="max-w200" placeholder="上下位置"></el-input>
            </div>
          </el-form-item>
          <!-- 图标数量设置 -->
          <el-form-item label="图标数量">
            <div class="d-s-r">
              <el-button type="text" @click="editIcon" :disabled="form.iconL.length <= 0">
                <i class="el-icon-minus"></i>
              </el-button>
              <span class="mr-10 ml-10">{{ form.iconL.length }}</span>
              <el-button type="text" @click="addIcon">
                <i class="el-icon-plus"></i>
              </el-button>
            </div>
          </el-form-item>
          <!-- 图标设置 -->
          <div v-for="(item, index) in form.iconL" :key="'icon_setting' + index" class="icon-setting">
            <el-form-item :label="'图标' + (index + 1)">
              <el-button type="primary" @click="openUpload(3, index)">上传图片</el-button>
            </el-form-item>
            <el-form-item :label="'图标' + (index + 1) + '尺寸'">
              <div class="d-s-r">
                <el-input v-model.number="form.iconL[index].width" min="10" type="number" class="max-w200" placeholder="宽度"></el-input>
                <span class="mr-10">x</span>
                <el-input v-model.number="form.iconL[index].height" min="10" type="number" class="max-w200" placeholder="高度"></el-input>
              </div>
            </el-form-item>
            <el-form-item :label="'图标' + (index + 1) + '位置'">
              <div class="d-s-r">
                <el-input v-model.number="form.iconL[index].left" min="0" type="number" class="max-w200" placeholder="左右位置"></el-input>
                <span class="mr-10">x</span>
                <el-input v-model.number="form.iconL[index].top" min="0" type="number" class="max-w200" placeholder="上下位置"></el-input>
              </div>
            </el-form-item>
          </div>
          <!-- 公司数量设置 -->
          <el-form-item label="公司数量">
            <div class="d-s-r">
              <el-button type="text" @click="editUnit" :disabled="form.unit.length <= 0">
                <i class="el-icon-minus"></i>
              </el-button>
              <span class="mr-10 ml-10">{{ form.unit.length }}</span>
              <el-button type="text" @click="addUnit">
                <i class="el-icon-plus"></i>
              </el-button>
            </div>
          </el-form-item>
          <!-- 公司设置 -->
          <div v-for="(item, index) in form.unit" :key="'unit_setting' + index" class="unit-setting">
            <el-form-item :label="'公司' + (index + 1) + '字体大小'">
              <el-input v-model.number="form.unit[index].fontSize" min="12" type="number" class="max-w460"></el-input>
            </el-form-item>
            <el-form-item :label="'公司' + (index + 1) + '字体颜色'">
              <el-color-picker v-model="form.unit[index].color"></el-color-picker>
            </el-form-item>
            <el-form-item :label="'公司' + (index + 1) + '位置'">
              <div class="d-s-r">
                <el-input v-model.number="form.unit[index].left" min="0" type="number" class="max-w200" placeholder="左右位置"></el-input>
                <span class="mr-10">x</span>
                <el-input v-model.number="form.unit[index].top" min="0" type="number" class="max-w200" placeholder="上下位置"></el-input>
              </div>
            </el-form-item>
          </div>
          <!-- 职位数量设置 -->
          <el-form-item label="职位数量">
            <div class="d-s-r">
              <el-button type="text" @click="editDuties" :disabled="form.duties.length <= 0">
                <i class="el-icon-minus"></i>
              </el-button>
              <span class="mr-10 ml-10">{{ form.duties.length }}</span>
              <el-button type="text" @click="addDuties">
                <i class="el-icon-plus"></i>
              </el-button>
            </div>
          </el-form-item>
          <!-- 职位设置 -->
          <div v-for="(item, index) in form.duties" :key="'duties_setting' + index" class="duties-setting">
            <el-form-item :label="'职位' + (index + 1) + '字体大小'">
              <el-input v-model.number="form.duties[index].fontSize" min="12" type="number" class="max-w460"></el-input>
            </el-form-item>
            <el-form-item :label="'职位' + (index + 1) + '字体颜色'">
              <el-color-picker v-model="form.duties[index].color"></el-color-picker>
            </el-form-item>
            <el-form-item :label="'职位' + (index + 1) + '位置'">
              <div class="d-s-r">
                <el-input v-model.number="form.duties[index].left" min="0" type="number" class="max-w200" placeholder="左右位置"></el-input>
                <span class="mr-10">x</span>
                <el-input v-model.number="form.duties[index].top" min="0" type="number" class="max-w200" placeholder="上下位置"></el-input>
              </div>
            </el-form-item>
          </div>
          <!-- 地址数量设置 -->
          <el-form-item label="地址数量">
            <div class="d-s-r">
              <el-button type="text" @click="editAddress" :disabled="form.address.length <= 0">
                <i class="el-icon-minus"></i>
              </el-button>
              <span class="mr-10 ml-10">{{ form.address.length }}</span>
              <el-button type="text" @click="addAddress">
                <i class="el-icon-plus"></i>
              </el-button>
            </div>
          </el-form-item>
          <!-- 地址设置 -->
          <div v-for="(item, index) in form.address" :key="'address_setting' + index" class="address-setting">
            <el-form-item :label="'地址' + (index + 1) + '字体大小'">
              <el-input v-model.number="form.address[index].fontSize" min="12" type="number" class="max-w460"></el-input>
            </el-form-item>
            <el-form-item :label="'地址' + (index + 1) + '字体颜色'">
              <el-color-picker v-model="form.address[index].color"></el-color-picker>
            </el-form-item>
            <el-form-item :label="'地址' + (index + 1) + '位置'">
              <div class="d-s-r">
                <el-input v-model.number="form.address[index].left" min="0" type="number" class="max-w200" placeholder="左右位置"></el-input>
                <span class="mr-10">x</span>
                <el-input v-model.number="form.address[index].top" min="0" type="number" class="max-w200" placeholder="上下位置"></el-input>
              </div>
            </el-form-item>
          </div>
          <!-- 微信设置 -->
          <el-form-item label="微信字体大小">
            <el-input v-model.number="form.wechat.fontSize" min="12" type="number" class="max-w460"></el-input>
          </el-form-item>
          <el-form-item label="微信字体颜色">
            <el-color-picker v-model="form.wechat.color"></el-color-picker>
          </el-form-item>
          <el-form-item label="微信位置">
            <div class="d-s-r">
              <el-input v-model.number="form.wechat.left" min="0" type="number" class="max-w200" placeholder="左右位置"></el-input>
              <span class="mr-10">x</span>
              <el-input v-model.number="form.wechat.top" min="0" type="number" class="max-w200" placeholder="上下位置"></el-input>
            </div>
          </el-form-item>
          <!-- 邮箱设置 -->
          <el-form-item label="邮箱字体大小">
            <el-input v-model.number="form.mailbox.fontSize" min="12" type="number" class="max-w460"></el-input>
          </el-form-item>
          <el-form-item label="邮箱字体颜色">
            <el-color-picker v-model="form.mailbox.color"></el-color-picker>
          </el-form-item>
          <el-form-item label="邮箱位置">
            <div class="d-s-r">
              <el-input v-model.number="form.mailbox.left" min="0" type="number" class="max-w200" placeholder="左右位置"></el-input>
              <span class="mr-10">x</span>
              <el-input v-model.number="form.mailbox.top" min="0" type="number" class="max-w200" placeholder="上下位置"></el-input>
            </div>
          </el-form-item>
          <!-- 电话设置 -->
          <el-form-item label="电话字体大小">
            <el-input v-model.number="form.phone.fontSize" min="12" type="number" class="max-w460"></el-input>
          </el-form-item>
          <el-form-item label="电话字体颜色">
            <el-color-picker v-model="form.phone.color"></el-color-picker>
          </el-form-item>
          <el-form-item label="电话位置">
            <div class="d-s-r">
              <el-input v-model.number="form.phone.left" min="0" type="number" class="max-w200" placeholder="左右位置"></el-input>
              <span class="mr-10">x</span>
              <el-input v-model.number="form.phone.top" min="0" type="number" class="max-w200" placeholder="上下位置"></el-input>
            </div>
          </el-form-item>
          <!-- 提交按钮 -->
          <div class="common-button-wrapper">
            <el-button @click="back">返回</el-button>
            <el-button type="primary" @click="onSubmit" :loading="loading">提交</el-button>
          </div>
        </el-form>
      </div>
    </div>
    <!-- 上传图片组件 -->
    <Upload v-if="isupload" :isupload="isupload" :type="uploadType" :index="uploadIndex" @returnImgs="returnImgsFunc">上传图片</Upload>
  </div>
</template>
<script>
import Upload from '@/components/file/Upload';
import BusinessApi from '@/api/business';
export default {
  components: {
    Upload
  },
  data() {
    return {
      loading: false,
      isupload: false,
      uploadType: 1,
      uploadIndex: 0,
      form: {
        backdrop: {
          src: '',
          height: 0,
          width: 0,
          type: 'backdrop'
        },
        is_business: 0,
        name: {
          fontSize: 14,
          color: '#000000',
          left: 232,
          top: 13,
          fontWeight: 400,
          type: 'text'
        },
        avatar: {
          width: 70,
          style: 'circle',
          left: 380,
          top: 37,
          display: '1',
          src: '',
          type: 'avatar'
        },
        logo: {
          width: 70,
          height: 27,
          style: 'square',
          left: 22,
          top: 24,
          display: '1',
          src: '',
          type: 'image'
        },
        mobile: {
          fontSize: 14,
          color: '#000000',
          left: 192,
          top: 43,
          fontWeight: 400
        },
        address: [
          {
            fontSize: 14,
            color: '#000000',
            left: 133,
            top: 206,
            fontWeight: 400,
            type: 'text'
          }
        ],
        unit: [
          {
            fontSize: 14,
            color: '#000000',
            left: 133,
            top: 167,
            fontWeight: 100,
            type: 'text'
          }
        ],
        duties: [
          {
            fontSize: 14,
            color: '#000000',
            left: 260,
            top: 167,
            fontWeight: 400,
            type: 'text'
          }
        ],
        position: [],
        wechat: {
          fontSize: 14,
          color: '#000000',
          left: 205,
          top: 65,
          fontWeight: 400,
          type: 'text'
        },
        mailbox: {
          fontSize: 14,
          color: '#000000',
          left: 205,
          top: 104,
          fontWeight: 400,
          type: 'text'
        },
        phone: {
          fontSize: 14,
          color: '#000000',
          left: 205,
          top: 84,
          fontWeight: 400,
          type: 'text'
        },
        positionNum: 0,
        iconL: []
      }
    };
  },
  created() {
    this.loadData();
    // 在组件创建后初始化拖拽事件
    this.$nextTick(() => {
      // 通过遍历this.form对象动态调用dragEventArray方法
      Object.keys(this.form).forEach(key => {
          this.dragEventArray(key);
      });
    });
  },
  directives: {
    drag: {
      inserted: function(el, binding) {
        // 为元素添加drag和对应的type类名,方便dragEventArray方法选择
        const type = binding.value.type;
        el.classList.add('drag');
        el.classList.add(type);
        // 避免指令和dragEventArray方法重复绑定拖拽事件
        // 实际的拖拽逻辑由dragEventArray方法处理
        el.style.position = 'absolute';
      }
    }
  },
  methods: {
    // 拖拽事件初始化方法,用于处理数组类型元素的拖拽
    dragEventArray(type) {
      // 使用类选择器获取对应的元素,更准确高效
      const elements = document.querySelectorAll(`.${type}`);
      elements.forEach((el, index) => {
        // 检查是否已经绑定过拖拽事件,避免重复绑定
        if (el.getAttribute('drag-handler') === 'true') return;
        el.setAttribute('drag-handler', 'true');
        // 为每个元素添加拖拽事件
        el.onmousedown = (event) => {
          event.preventDefault();
          // 计算鼠标按下位置与元素左上角的偏移
          let sentX = event.clientX - el.offsetLeft;
          let sentY = event.clientY - el.offsetTop;
          // 获取父容器的边界
          let parent = el.parentElement;
          let l = 0;
          let t = 0;
          let r = parent.offsetWidth - el.offsetWidth;
          let b = parent.offsetHeight - el.offsetHeight;
          document.onmousemove = (event) => {
            event.preventDefault();
            // 计算新位置
            let slideLeft = event.clientX - sentX;
            let slideTop = event.clientY - sentY;
            // 限制在父容器内
            slideLeft <= l && (slideLeft = l);
            slideLeft >= r && (slideLeft = r);
            slideTop <= t && (slideTop = t);
            slideTop >= b && (slideTop = b);
            // 更新位置
              if (Array.isArray(this.form[type]) && this.form[type][index]) {
                // 使用Vue.set确保响应式更新,这样右侧表单的位置输入框也能同步更新
                this.$set(this.form[type][index], 'left', slideLeft);
                this.$set(this.form[type][index], 'top', slideTop);
                // 直接更新DOM样式,确保视觉效果即时生效
                el.style.left = slideLeft + 'px';
                el.style.top = slideTop + 'px';
                // 强制Vue更新,确保表单输入框同步更新
                this.$forceUpdate();
              } else { // 使用Vue.set确保响应式更新,这样右侧表单的位置输入框也能同步更新
                this.$set(this.form[type], 'left', slideLeft);
                this.$set(this.form[type], 'top', slideTop);
                // 直接更新DOM样式,确保视觉效果即时生效
                el.style.left = slideLeft + 'px';
                el.style.top = slideTop + 'px';
                // 强制Vue更新,确保表单输入框同步更新
                this.$forceUpdate();
              }
          };
          document.onmouseup = () => {
            document.onmousemove = null;
            document.onmouseup = null;
          };
          return false;
        };
      });
    },
    // 加载数据
    loadData() {
      let self = this;
      self.loading = true;
      BusinessApi.templateDefault({}, true)
        .then(res => {
          if (res.data.data) {
            try {
              const data = JSON.parse(res.data.data);
              self.form = data;
            } catch (e) {
              console.error('解析数据失败', e);
            }
          }
          self.loading = false;
        })
        .catch(error => {
          self.loading = false;
          console.error('加载数据失败', error);
        });
    },
    // 拖动处理
    dragDiv(x, y, type, index) {
      // 确保坐标值有效
      x = Math.max(0, x || 0);
      y = Math.max(0, y || 0);
      // 确保form对象存在
      if (!this.form) {
        console.warn('Form object not initialized');
        return;
      }
      switch (type) {
        case 'avatar':
          if (this.form.avatar) {
            this.form.avatar.left = x;
            this.form.avatar.top = y;
          }
          break;
        case 'logo':
          if (this.form.logo) {
            this.form.logo.left = x;
            this.form.logo.top = y;
          }
          break;
        case 'name':
          if (this.form.name) {
            this.form.name.left = x;
            this.form.name.top = y;
          }
          break;
        case 'mobile':
          if (this.form.mobile) {
            this.form.mobile.left = x;
            this.form.mobile.top = y;
          }
          break;
        case 'wechat':
          if (this.form.wechat) {
            this.form.wechat.left = x;
            this.form.wechat.top = y;
          }
          break;
        case 'mailbox':
          if (this.form.mailbox) {
            this.form.mailbox.left = x;
            this.form.mailbox.top = y;
          }
          break;
        case 'phone':
          if (this.form.phone) {
            this.form.phone.left = x;
            this.form.phone.top = y;
          }
          break;
        case 'unit':
          // 增强公司元素的拖拽处理
          if (Array.isArray(this.form.unit) && this.form.unit[index]) {
            // 确保对象存在并具有left和top属性
            if (!this.form.unit[index].left && this.form.unit[index].left !== 0) {
              this.form.unit[index].left = 0;
            }
            if (!this.form.unit[index].top && this.form.unit[index].top !== 0) {
              this.form.unit[index].top = 0;
            }
            this.form.unit[index].left = x;
            this.form.unit[index].top = y;
          } else {
            console.warn('Unit element not found at index:', index);
          }
          break;
        case 'duties':
          // 增强职位元素的拖拽处理
          if (Array.isArray(this.form.duties) && this.form.duties[index]) {
            if (!this.form.duties[index].left && this.form.duties[index].left !== 0) {
              this.form.duties[index].left = 0;
            }
            if (!this.form.duties[index].top && this.form.duties[index].top !== 0) {
              this.form.duties[index].top = 0;
            }
            this.form.duties[index].left = x;
            this.form.duties[index].top = y;
          } else {
            console.warn('Duties element not found at index:', index);
          }
          break;
        case 'address':
          // 增强地址元素的拖拽处理
          if (Array.isArray(this.form.address) && this.form.address[index]) {
            if (!this.form.address[index].left && this.form.address[index].left !== 0) {
              this.form.address[index].left = 0;
            }
            if (!this.form.address[index].top && this.form.address[index].top !== 0) {
              this.form.address[index].top = 0;
            }
            this.form.address[index].left = x;
            this.form.address[index].top = y;
          } else {
            console.warn('Address element not found at index:', index);
          }
          break;
        case 'iconL':
          if (Array.isArray(this.form.iconL) && this.form.iconL[index]) {
            this.form.iconL[index].left = x;
            this.form.iconL[index].top = y;
          }
          break;
        default:
          console.warn('Unknown drag type:', type);
      }
    },
    // 打开上传
    openUpload(type, index) {
      this.uploadType = type;
      this.uploadIndex = index;
      this.isupload = true;
    },
    // 返回图片
    returnImgsFunc(e) {
      if (e && e.length > 0) {
        switch (this.uploadType) {
          case 1: // 背景图
            this.form.backdrop.src = e[0].file_path;
            // 这里可以添加获取图片尺寸的逻辑
            break;
          case 2: // Logo
            this.form.logo.src = e[0].file_path;
            break;
          case 3: // 图标
            if (!this.form.iconL[this.uploadIndex]) {
              this.form.iconL[this.uploadIndex] = {};
            }
            this.form.iconL[this.uploadIndex].src = e[0].file_path;
            break;
        }
      }
      this.isupload = false;
    },
    // 添加图标
    addIcon() {
      this.form.iconL.push({
        src: '',
        width: 30,
        height: 30,
        left: 100,
        top: 100
      });// 添加后重新初始化拖拽事件
      this.$nextTick(() => {
        this.dragEventArray('iconL');
      });
    },
    // 减少图标
    editIcon() {
      if (this.form.iconL.length > 0) {
        this.form.iconL.pop();
      }
    },
    // 添加公司
    addUnit() {
      const index = this.form.unit.length;
      this.form.unit.push({
        fontSize: 14,
        color: '#000000',
        left: 133,
        top: 167 + (index * 20),
        fontWeight: 100,
        type: 'text'
      });
      // 添加后重新初始化拖拽事件
      this.$nextTick(() => {
        this.dragEventArray('unit');
      });
    },
    // 减少公司
    editUnit() {
      if (this.form.unit.length > 0) {
        this.form.unit.pop();
      }
    },
    // 添加职位
    addDuties() {
      const index = this.form.duties.length;
      this.form.duties.push({
        fontSize: 14,
        color: '#000000',
        left: 260,
        top: 167 + (index * 20),
        fontWeight: 400,
        type: 'text'
      });
      // 添加后重新初始化拖拽事件
      this.$nextTick(() => {
        this.dragEventArray('duties');
      });
    },
    // 减少职位
    editDuties() {
      if (this.form.duties.length > 0) {
        this.form.duties.pop();
      }
    },
    // 添加地址
    addAddress() {
      const index = this.form.address.length;
      this.form.address.push({
        fontSize: 14,
        color: '#000000',
        left: 133,
        top: 206 + (index * 20),
        fontWeight: 400,
        type: 'text'
      });
      // 添加后重新初始化拖拽事件
      this.$nextTick(() => {
        this.dragEventArray('address');
      });
    },
    // 减少地址
    editAddress() {
      if (this.form.address.length > 0) {
        this.form.address.pop();
      }
    },
    // 提交表单
    onSubmit() {
      let self = this;
      self.$refs.form.validate(valid => {
        if (valid) {
          self.loading = true;
          // 确保必要字段存在
          if (!self.form.backdrop.src) {
            self.$message.error('请上传背景图');
            self.loading = false;
            return;
          }
          BusinessApi.templateSave({
            template: self.form
          }, true)
            .then(res => {
              self.loading = false;
              self.$message({
                message: '保存成功',
                type: 'success'
              });
              self.$router.push('/plus/business/template/index');
            })
            .catch(error => {
              self.loading = false;
              self.$message.error('保存失败,请重试');
            });
        }
      });
    },
    // 返回
    back() {
      this.$router.push('/plus/business/template/index');
    }
  }
};
</script>
<style scoped>
.poster-box {
  display: flex;
  flex-wrap: wrap;
}
.poster-box .left-box {
  position: relative;
  overflow: hidden;
  margin: 0 30px;
  border: 1px solid #e0e0e0;
  border-radius: 8px;
  background-color: #f5f5f5;
}
.poster-box .left-box .img img {
  width: auto;
  object-fit: cover;
}
.poster-box .left-box .userinfo {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
}
.poster-box .left-box .pa {
  position: absolute;
  cursor: move;
}
.poster-box .left-box .photo,
.poster-box .left-box .logo,
.poster-box .left-box .icon {
  display: flex;
  justify-content: center;
  align-items: center;
  overflow: hidden;
  background: #ffffff;
  border: 1px solid #e0e0e0;
}
.poster-box .left-box .photo.radius,
.poster-box .left-box .logo.radius,
.poster-box .left-box .icon.radius {
  border-radius: 50%;
}
.poster-box .left-box .photo img,
.poster-box .left-box .logo img,
.poster-box .left-box .icon img {
  width: 100%;
  height: 100%;
  object-fit: contain;
}
.poster-box .left-box .name,
.poster-box .left-box .mobile,
.poster-box .left-box .unit,
.poster-box .left-box .duties,
.poster-box .left-box .address,
.poster-box .left-box .wechat,
.poster-box .left-box .mailbox,
.poster-box .left-box .phone {
  padding: 2px 5px;
  white-space: nowrap;
  border-radius: 4px;
}
.poster-box .right-box {
  flex: 1;
  min-width: 400px;
  max-height: 800px;
  overflow-y: auto;
  padding: 0 20px;
}
.tips {
  margin-top: 5px;
  color: #999;
  font-size: 12px;
}
.d-s-r {
  display: flex;
  align-items: center;
}
.max-w460 {
  max-width: 460px;
}
.max-w200 {
  max-width: 200px;
}
.mr-10 {
  margin-right: 10px;
}
.ml-10 {
  margin-left: 10px;
}
.icon-setting {
  border: 1px solid #e0e0e0;
  padding: 10px;
  margin-bottom: 10px;
  border-radius: 4px;
}
</style>
shop_vue/src/views/plus/business/template/edit.vue
New file
@@ -0,0 +1,1014 @@
<template>
  <!--
    作者:系统自动生成
    时间:当前日期
    描述:插件中心-名片模板-编辑模板
  -->
  <div class="user" v-loading="loading">
    <div class="common-form">名片模板编辑</div>
    <div class="poster-box d-s-s">
      <div class="left-box">
        <div v-if="form.backdrop" class="img"><img v-img-url="form.backdrop.src" /></div>
        <div class="userinfo">
          <!-- 头像 -->
          <div v-if="form.avatar.display == 1"
               class="photo pa"
               v-drag="{type:'avatar',obj:this}"
               :class="{ radius: form.avatar.style == 'circle' }"
               :style="'width:' + form.avatar.width + 'px;height:' + form.avatar.width + 'px;top:' + form.avatar.top + 'px;left:' + form.avatar.left + 'px;background-color:#f0f0f0;display:flex;align-items:center;justify-content:center;color:#999'">
            <span>头像</span>
          </div>
          <!-- Logo -->
          <div v-if="form.logo.display == 1"
               class="logo pa"
               v-drag="{type:'logo',obj:this}"
               :class="{ radius: form.logo.style == 'circle' }"
               :style="'width:' + form.logo.width + 'px;height:' + form.logo.height + 'px;top:' + form.logo.top + 'px;left:' + form.logo.left + 'px;'">
            <img v-img-url="form.logo.src" alt="" />
          </div>
          <!-- 姓名 -->
          <div class="name pa"
               v-drag="{type:'name',obj:this}"
               :style="'font-size:' + form.name.fontSize + 'px;color:' + form.name.color + ';top:' + form.name.top + 'px;left:' + form.name.left + 'px;'">
            这里是姓名
          </div>
          <!-- 手机 -->
          <div class="mobile pa"
               v-drag="{type:'mobile',obj:this}"
               :style="'font-size:' + form.mobile.fontSize + 'px;color:' + form.mobile.color + ';top:' + form.mobile.top + 'px;left:' + form.mobile.left + 'px;'">
            手机:134xxxxxxxx
          </div>
          <!-- 公司 -->
          <div v-for="(item, index) in form.unit" :key="'unit' + index"
               :class="'unit' + index + ' pa unit'"
               v-drag="{type:'unit', index:index, obj:this}"
               :style="'font-size:' + form.unit[index].fontSize + 'px;color:' + form.unit[index].color + ';top:' + form.unit[index].top + 'px;left:' + form.unit[index].left + 'px;'">
            这是公司{{index+1}}
          </div>
          <!-- 职位 -->
          <div v-for="(item, index) in form.duties" :key="'duties' + index"
               :class="'duties' + index + ' pa duties'"
               v-drag="{type:'duties', index:index, obj:this}"
               :style="'font-size:' + form.duties[index].fontSize + 'px;color:' + form.duties[index].color + ';top:' + form.duties[index].top + 'px;left:' + form.duties[index].left + 'px;'">
            这是职位{{index+1}}
          </div>
          <!-- 地址 -->
          <div v-for="(item, index) in form.address" :key="'address' + index"
               :class="'address' + index + ' pa address'"
               v-drag="{type:'address', index:index, obj:this}"
               :style="'font-size:' + form.address[index].fontSize + 'px;color:' + form.address[index].color + ';top:' + form.address[index].top + 'px;left:' + form.address[index].left + 'px;'">
            地址:广西壮族自治区南宁市江南区壮锦大道八桂绿城·龙湖御景-A栋-2单元{{index+1}}号
          </div>
          <!-- 微信 -->
          <div class="wechat pa"
               v-drag="{type:'wechat',obj:this}"
               :style="'font-size:' + form.wechat.fontSize + 'px;color:' + form.wechat.color + ';top:' + form.wechat.top + 'px;left:' + form.wechat.left + 'px;'">
            微信:134xxxxxxxx
          </div>
          <!-- 邮箱 -->
          <div class="mailbox pa"
               v-drag="{type:'mailbox',obj:this}"
               :style="'font-size:' + form.mailbox.fontSize + 'px;color:' + form.mailbox.color + ';top:' + form.mailbox.top + 'px;left:' + form.mailbox.left + 'px;'">
            邮箱:134xxxxxxxx@.xxx.com
          </div>
          <!-- 电话 -->
          <div class="phone pa"
               v-drag="{type:'phone',obj:this}"
               :style="'font-size:' + form.phone.fontSize + 'px;color:' + form.phone.color + ';top:' + form.phone.top + 'px;left:' + form.phone.left + 'px;'">
            电话:xxx-xxx-xxx
          </div>
          <!-- 自定义图标 -->
          <div v-for="(item, index) in form.iconL" :key="'iconL' + index"
               :class="'iconL' + index + ' pa icon'"
               v-drag="{type:'iconL', index:index, obj:this}"
               :style="'width:' + form.iconL[index].width + 'px;height:' + form.iconL[index].height + 'px;top:' + form.iconL[index].top + 'px;left:' + form.iconL[index].left + 'px;'">
            <img v-img-url="form.iconL[index].src" alt="" />
          </div>
        </div>
      </div>
      <div class="right-box flex-1">
        <el-form size="small" ref="form" :model="form" label-width="150px">
          <!-- 背景图 -->
          <el-form-item label="海报背景图">
            <el-button type="primary" @click="openUpload(1)">上传图片</el-button>
            <div class="tips">尺寸建议:宽750像素 高大于(等于)1200像素</div>
          </el-form-item>
          <!-- 头像设置 -->
          <el-form-item label="是否显示头像">
            <el-radio v-model="form.avatar.display" label="1">显示</el-radio>
            <el-radio v-model="form.avatar.display" label="0">隐藏</el-radio>
          </el-form-item>
          <el-form-item v-if="form.avatar.display == 1" label="头像宽度" prop="avatar.width" :rules="[{ required: true, message: '请输入头像宽度' }]">
            <el-input v-model.number="form.avatar.width" min="30" type="number" class="max-w460"></el-input>
          </el-form-item>
          <el-form-item v-if="form.avatar.display == 1" label="头像样式">
            <el-radio v-model="form.avatar.style" label="square">正方形</el-radio>
            <el-radio v-model="form.avatar.style" label="circle">圆形</el-radio>
          </el-form-item>
          <el-form-item v-if="form.avatar.display == 1" label="头像位置">
            <div class="d-s-r">
              <el-input v-model.number="form.avatar.left" min="0" type="number" class="max-w200" placeholder="左右位置"></el-input>
              <span class="mr-10">x</span>
              <el-input v-model.number="form.avatar.top" min="0" type="number" class="max-w200" placeholder="上下位置"></el-input>
            </div>
          </el-form-item>
          <!-- Logo设置 -->
          <el-form-item label="是否显示Logo">
            <el-radio v-model="form.logo.display" label="1">显示</el-radio>
            <el-radio v-model="form.logo.display" label="0">隐藏</el-radio>
          </el-form-item>
          <el-form-item v-if="form.logo.display == 1" label="Logo图片">
            <el-button type="primary" @click="openUpload(2)">上传图片</el-button>
          </el-form-item>
          <el-form-item v-if="form.logo.display == 1" label="Logo尺寸" prop="logo.width" :rules="[{ required: true, message: '请输入Logo宽度' }]">
            <div class="d-s-r">
              <el-input v-model.number="form.logo.width" min="10" type="number" class="max-w200" placeholder="宽度"></el-input>
              <span class="mr-10">x</span>
              <el-input v-model.number="form.logo.height" min="10" type="number" class="max-w200" placeholder="高度"></el-input>
            </div>
          </el-form-item>
          <el-form-item v-if="form.logo.display == 1" label="Logo样式">
            <el-radio v-model="form.logo.style" label="square">正方形</el-radio>
            <el-radio v-model="form.logo.style" label="circle">圆形</el-radio>
          </el-form-item>
          <el-form-item v-if="form.logo.display == 1" label="Logo位置">
            <div class="d-s-r">
              <el-input v-model.number="form.logo.left" min="0" type="number" class="max-w200" placeholder="左右位置"></el-input>
              <span class="mr-10">x</span>
              <el-input v-model.number="form.logo.top" min="0" type="number" class="max-w200" placeholder="上下位置"></el-input>
            </div>
          </el-form-item>
          <!-- 姓名设置 -->
          <el-form-item label="姓名字体大小" prop="name.fontSize" :rules="[{ required: true, message: '请输入字体大小' }]">
            <el-input v-model.number="form.name.fontSize" min="12" type="number" class="max-w460"></el-input>
          </el-form-item>
          <el-form-item label="姓名字体颜色">
            <el-color-picker v-model="form.name.color"></el-color-picker>
          </el-form-item>
          <el-form-item label="姓名位置">
            <div class="d-s-r">
              <el-input v-model.number="form.name.left" min="0" type="number" class="max-w200" placeholder="左右位置"></el-input>
              <span class="mr-10">x</span>
              <el-input v-model.number="form.name.top" min="0" type="number" class="max-w200" placeholder="上下位置"></el-input>
            </div>
          </el-form-item>
          <!-- 手机设置 -->
          <el-form-item label="手机字体大小" prop="mobile.fontSize" :rules="[{ required: true, message: '请输入字体大小' }]">
            <el-input v-model.number="form.mobile.fontSize" min="12" type="number" class="max-w460"></el-input>
          </el-form-item>
          <el-form-item label="手机字体颜色">
            <el-color-picker v-model="form.mobile.color"></el-color-picker>
          </el-form-item>
          <el-form-item label="手机位置">
            <div class="d-s-r">
              <el-input v-model.number="form.mobile.left" min="0" type="number" class="max-w200" placeholder="左右位置"></el-input>
              <span class="mr-10">x</span>
              <el-input v-model.number="form.mobile.top" min="0" type="number" class="max-w200" placeholder="上下位置"></el-input>
            </div>
          </el-form-item>
          <!-- 图标数量设置 -->
          <el-form-item label="图标数量">
            <div class="d-s-r">
              <el-button type="text" @click="editIcon" :disabled="form.iconL.length <= 0">
                <i class="el-icon-minus"></i>
              </el-button>
              <span class="mr-10 ml-10">{{ form.iconL.length }}</span>
              <el-button type="text" @click="addIcon">
                <i class="el-icon-plus"></i>
              </el-button>
            </div>
          </el-form-item>
          <!-- 图标设置 -->
          <div v-for="(item, index) in form.iconL" :key="'icon_setting' + index" class="icon-setting">
            <el-form-item :label="'图标' + (index + 1)">
              <el-button type="primary" @click="openUpload(3, index)">上传图片</el-button>
            </el-form-item>
            <el-form-item :label="'图标' + (index + 1) + '尺寸'">
              <div class="d-s-r">
                <el-input v-model.number="form.iconL[index].width" min="10" type="number" class="max-w200" placeholder="宽度"></el-input>
                <span class="mr-10">x</span>
                <el-input v-model.number="form.iconL[index].height" min="10" type="number" class="max-w200" placeholder="高度"></el-input>
              </div>
            </el-form-item>
            <el-form-item :label="'图标' + (index + 1) + '位置'">
              <div class="d-s-r">
                <el-input v-model.number="form.iconL[index].left" min="0" type="number" class="max-w200" placeholder="左右位置"></el-input>
                <span class="mr-10">x</span>
                <el-input v-model.number="form.iconL[index].top" min="0" type="number" class="max-w200" placeholder="上下位置"></el-input>
              </div>
            </el-form-item>
          </div>
          <!-- 公司数量设置 -->
          <el-form-item label="公司数量">
            <div class="d-s-r">
              <el-button type="text" @click="editUnit" :disabled="form.unit.length <= 0">
                <i class="el-icon-minus"></i>
              </el-button>
              <span class="mr-10 ml-10">{{ form.unit.length }}</span>
              <el-button type="text" @click="addUnit">
                <i class="el-icon-plus"></i>
              </el-button>
            </div>
          </el-form-item>
          <!-- 公司设置 -->
          <div v-for="(item, index) in form.unit" :key="'unit_setting' + index" class="unit-setting">
            <el-form-item :label="'公司' + (index + 1) + '字体大小'">
              <el-input v-model.number="form.unit[index].fontSize" min="12" type="number" class="max-w460"></el-input>
            </el-form-item>
            <el-form-item :label="'公司' + (index + 1) + '字体颜色'">
              <el-color-picker v-model="form.unit[index].color"></el-color-picker>
            </el-form-item>
            <el-form-item :label="'公司' + (index + 1) + '位置'">
              <div class="d-s-r">
                <el-input v-model.number="form.unit[index].left" min="0" type="number" class="max-w200" placeholder="左右位置"></el-input>
                <span class="mr-10">x</span>
                <el-input v-model.number="form.unit[index].top" min="0" type="number" class="max-w200" placeholder="上下位置"></el-input>
              </div>
            </el-form-item>
          </div>
          <!-- 职位数量设置 -->
          <el-form-item label="职位数量">
            <div class="d-s-r">
              <el-button type="text" @click="editDuties" :disabled="form.duties.length <= 0">
                <i class="el-icon-minus"></i>
              </el-button>
              <span class="mr-10 ml-10">{{ form.duties.length }}</span>
              <el-button type="text" @click="addDuties">
                <i class="el-icon-plus"></i>
              </el-button>
            </div>
          </el-form-item>
          <!-- 职位设置 -->
          <div v-for="(item, index) in form.duties" :key="'duties_setting' + index" class="duties-setting">
            <el-form-item :label="'职位' + (index + 1) + '字体大小'">
              <el-input v-model.number="form.duties[index].fontSize" min="12" type="number" class="max-w460"></el-input>
            </el-form-item>
            <el-form-item :label="'职位' + (index + 1) + '字体颜色'">
              <el-color-picker v-model="form.duties[index].color"></el-color-picker>
            </el-form-item>
            <el-form-item :label="'职位' + (index + 1) + '位置'">
              <div class="d-s-r">
                <el-input v-model.number="form.duties[index].left" min="0" type="number" class="max-w200" placeholder="左右位置"></el-input>
                <span class="mr-10">x</span>
                <el-input v-model.number="form.duties[index].top" min="0" type="number" class="max-w200" placeholder="上下位置"></el-input>
              </div>
            </el-form-item>
          </div>
          <!-- 地址数量设置 -->
          <el-form-item label="地址数量">
            <div class="d-s-r">
              <el-button type="text" @click="editAddress" :disabled="form.address.length <= 0">
                <i class="el-icon-minus"></i>
              </el-button>
              <span class="mr-10 ml-10">{{ form.address.length }}</span>
              <el-button type="text" @click="addAddress">
                <i class="el-icon-plus"></i>
              </el-button>
            </div>
          </el-form-item>
          <!-- 地址设置 -->
          <div v-for="(item, index) in form.address" :key="'address_setting' + index" class="address-setting">
            <el-form-item :label="'地址' + (index + 1) + '字体大小'">
              <el-input v-model.number="form.address[index].fontSize" min="12" type="number" class="max-w460"></el-input>
            </el-form-item>
            <el-form-item :label="'地址' + (index + 1) + '字体颜色'">
              <el-color-picker v-model="form.address[index].color"></el-color-picker>
            </el-form-item>
            <el-form-item :label="'地址' + (index + 1) + '位置'">
              <div class="d-s-r">
                <el-input v-model.number="form.address[index].left" min="0" type="number" class="max-w200" placeholder="左右位置"></el-input>
                <span class="mr-10">x</span>
                <el-input v-model.number="form.address[index].top" min="0" type="number" class="max-w200" placeholder="上下位置"></el-input>
              </div>
            </el-form-item>
          </div>
          <!-- 微信设置 -->
          <el-form-item label="微信字体大小">
            <el-input v-model.number="form.wechat.fontSize" min="12" type="number" class="max-w460"></el-input>
          </el-form-item>
          <el-form-item label="微信字体颜色">
            <el-color-picker v-model="form.wechat.color"></el-color-picker>
          </el-form-item>
          <el-form-item label="微信位置">
            <div class="d-s-r">
              <el-input v-model.number="form.wechat.left" min="0" type="number" class="max-w200" placeholder="左右位置"></el-input>
              <span class="mr-10">x</span>
              <el-input v-model.number="form.wechat.top" min="0" type="number" class="max-w200" placeholder="上下位置"></el-input>
            </div>
          </el-form-item>
          <!-- 邮箱设置 -->
          <el-form-item label="邮箱字体大小">
            <el-input v-model.number="form.mailbox.fontSize" min="12" type="number" class="max-w460"></el-input>
          </el-form-item>
          <el-form-item label="邮箱字体颜色">
            <el-color-picker v-model="form.mailbox.color"></el-color-picker>
          </el-form-item>
          <el-form-item label="邮箱位置">
            <div class="d-s-r">
              <el-input v-model.number="form.mailbox.left" min="0" type="number" class="max-w200" placeholder="左右位置"></el-input>
              <span class="mr-10">x</span>
              <el-input v-model.number="form.mailbox.top" min="0" type="number" class="max-w200" placeholder="上下位置"></el-input>
            </div>
          </el-form-item>
          <!-- 电话设置 -->
          <el-form-item label="电话字体大小">
            <el-input v-model.number="form.phone.fontSize" min="12" type="number" class="max-w460"></el-input>
          </el-form-item>
          <el-form-item label="电话字体颜色">
            <el-color-picker v-model="form.phone.color"></el-color-picker>
          </el-form-item>
          <el-form-item label="电话位置">
            <div class="d-s-r">
              <el-input v-model.number="form.phone.left" min="0" type="number" class="max-w200" placeholder="左右位置"></el-input>
              <span class="mr-10">x</span>
              <el-input v-model.number="form.phone.top" min="0" type="number" class="max-w200" placeholder="上下位置"></el-input>
            </div>
          </el-form-item>
          <!-- 提交按钮 -->
          <div class="common-button-wrapper">
            <el-button @click="back">返回</el-button>
            <el-button type="primary" @click="onSubmit" :loading="loading">提交</el-button>
          </div>
        </el-form>
      </div>
    </div>
    <!-- 上传图片组件 -->
    <Upload v-if="isupload" :isupload="isupload" :type="uploadType" :index="uploadIndex" @returnImgs="returnImgsFunc">上传图片</Upload>
  </div>
</template>
<script>
import Upload from '@/components/file/Upload';
import BusinessApi from '@/api/business';
export default {
  components: {
    Upload
  },
  data() {
    return {
      loading: false,
      isupload: false,
      uploadType: 1,
      uploadIndex: 0,
      form: {
        backdrop: {
          src: '',
          height: 0,
          width: 0,
          type: 'backdrop'
        },
        is_business: 0,
        name: {
          fontSize: 14,
          color: '#000000',
          left: 232,
          top: 13,
          fontWeight: 400,
          type: 'text'
        },
        avatar: {
          width: 70,
          style: 'circle',
          left: 37,
          top: 37,
          display: 1,
          src: '',
          type: 'avatar'
        },
        logo: {
          width: 70,
          height: 27,
          style: 'square',
          left: 22,
          src: '',
          top: 24,
          display: 1,
          type: 'image'
        },
        mobile: {
          fontSize: 14,
          color: '#000000',
          left: 192,
          top: 43,
          fontWeight: 400
        },
        address: [
          {
            fontSize: 14,
            color: '#000000',
            left: 133,
            top: 206,
            fontWeight: 400,
            type: 'text'
          }
        ],
        unit: [
          {
            fontSize: 14,
            color: '#000000',
            left: 133,
            top: 167,
            fontWeight: 100,
            type: 'text'
          }
        ],
        duties: [
          {
            fontSize: 14,
            color: '#000000',
            left: 260,
            top: 167,
            fontWeight: 400,
            type: 'text'
          }
        ],
        position: [],
        wechat: {
          fontSize: 14,
          color: '#000000',
          left: 205,
          top: 65,
          fontWeight: 400,
          type: 'text'
        },
        mailbox: {
          fontSize: 14,
          color: '#000000',
          left: 205,
          top: 104,
          fontWeight: 400,
          type: 'text'
        },
        phone: {
          fontSize: 14,
          color: '#000000',
          left: 205,
          top: 84,
          fontWeight: 400,
          type: 'text'
        },
        positionNum: 0,
        iconL: []
      }
    };
  },
  directives: {
    drag: {
      inserted(el, binding) {
        const { type, obj } = binding.value;
        // 添加类型相关的类名,用于拖拽事件的绑定
        el.classList.add('drag');
        el.classList.add(type);
        // 避免指令和dragEventArray方法重复绑定拖拽事件
        // 实际的拖拽逻辑由dragEventArray方法处理
        el.style.position = 'absolute';
      }
    }
  },
  created() {
    // 获取URL参数中的template_id
    this.template_id = this.$route.query.template_id;
    if (this.template_id) {
      this.loadData();
    }
  },
  mounted() {
    // 在组件挂载完成后,为所有可拖拽元素初始化拖拽事件
    this.$nextTick(() => {
      // 为数组类型的元素初始化拖拽事件
      Object.keys(this.form).forEach(key => {
          this.dragEventArray(key);
      });
    });
  },
  methods: {
    // 拖拽事件初始化方法,用于处理数组类型元素的拖拽
    dragEventArray(type) {
      // 使用类选择器获取对应的元素,更准确高效
      const elements = document.querySelectorAll(`.${type}`);
      elements.forEach((el, index) => {
        // 检查是否已经绑定过拖拽事件,避免重复绑定
        if (el.getAttribute('drag-handler') === 'true') return;
        el.setAttribute('drag-handler', 'true');
        // 为每个元素添加拖拽事件
        el.onmousedown = (event) => {
          event.preventDefault();
          // 计算鼠标按下位置与元素左上角的偏移
          let sentX = event.clientX - el.offsetLeft;
          let sentY = event.clientY - el.offsetTop;
          // 获取父容器的边界
          let parent = el.parentElement;
          let l = 0;
          let t = 0;
          let r = parent.offsetWidth - el.offsetWidth;
          let b = parent.offsetHeight - el.offsetHeight;
          document.onmousemove = (event) => {
            event.preventDefault();
            // 计算新位置
            let slideLeft = event.clientX - sentX;
            let slideTop = event.clientY - sentY;
            // 限制在父容器内
            slideLeft <= l && (slideLeft = l);
            slideLeft >= r && (slideLeft = r);
            slideTop <= t && (slideTop = t);
            slideTop >= b && (slideTop = b);
            // 更新位置
              if (Array.isArray(this.form[type]) && this.form[type][index]) {
                // 使用Vue.set确保响应式更新,这样右侧表单的位置输入框也能同步更新
                this.$set(this.form[type][index], 'left', slideLeft);
                this.$set(this.form[type][index], 'top', slideTop);
                // 直接更新DOM样式,确保视觉效果即时生效
                el.style.left = slideLeft + 'px';
                el.style.top = slideTop + 'px';
                // 强制Vue更新,确保表单输入框同步更新
                this.$forceUpdate();
              } else {
                // 使用Vue.set确保响应式更新,这样右侧表单的位置输入框也能同步更新
                this.$set(this.form[type], 'left', slideLeft);
                this.$set(this.form[type], 'top', slideTop);
                // 直接更新DOM样式,确保视觉效果即时生效
                el.style.left = slideLeft + 'px';
                el.style.top = slideTop + 'px';
                // 强制Vue更新,确保表单输入框同步更新
                this.$forceUpdate();
              }
          };
          document.onmouseup = () => {
            document.onmousemove = null;
            document.onmouseup = null;
          };
          return false;
        };
      });
    },
    // 加载数据
    loadData() {
      let self = this;
      self.loading = true;
      BusinessApi.templateDetail({template_id: self.template_id}, true)
        .then(res => {
          if (res.data.data) {
            try {
              const data = JSON.parse(res.data.data);
              // 确保所有必要的数组字段存在
              if (!data.unit) data.unit = [];
              if (!data.duties) data.duties = [];
              if (!data.address) data.address = [];
              if (!data.iconL) data.iconL = [];
              self.form = data;
              // 数据加载完成后,重新初始化拖拽事件
              self.$nextTick(() => {
                ['unit', 'duties', 'address', 'iconL'].forEach(type => {
                  self.dragEventArray(type);
                });
              });
            } catch (e) {
              console.error('解析数据失败', e);
            }
          }
          self.loading = false;
        })
        .catch(error => {
          self.loading = false;
          console.error('加载数据失败', error);
        });
    },
    // 拖动处理
    dragDiv(x, y, type, index) {
      // 确保坐标值有效
      x = Math.max(0, x || 0);
      y = Math.max(0, y || 0);
      // 确保form对象存在
      if (!this.form) {
        console.warn('Form object not initialized');
        return;
      }
      switch (type) {
        case 'avatar':
          if (this.form.avatar) {
            this.form.avatar.left = x;
            this.form.avatar.top = y;
          }
          break;
        case 'logo':
          if (this.form.logo) {
            this.form.logo.left = x;
            this.form.logo.top = y;
          }
          break;
        case 'name':
          if (this.form.name) {
            this.form.name.left = x;
            this.form.name.top = y;
          }
          break;
        case 'mobile':
          if (this.form.mobile) {
            this.form.mobile.left = x;
            this.form.mobile.top = y;
          }
          break;
        case 'wechat':
          if (this.form.wechat) {
            this.form.wechat.left = x;
            this.form.wechat.top = y;
          }
          break;
        case 'mailbox':
          if (this.form.mailbox) {
            this.form.mailbox.left = x;
            this.form.mailbox.top = y;
          }
          break;
        case 'phone':
          if (this.form.phone) {
            this.form.phone.left = x;
            this.form.phone.top = y;
          }
          break;
        case 'unit':
          // 增强公司元素的拖拽处理
          if (Array.isArray(this.form.unit) && this.form.unit[index]) {
            // 确保对象存在并具有left和top属性
            if (!this.form.unit[index].left && this.form.unit[index].left !== 0) {
              this.form.unit[index].left = 0;
            }
            if (!this.form.unit[index].top && this.form.unit[index].top !== 0) {
              this.form.unit[index].top = 0;
            }
            this.form.unit[index].left = x;
            this.form.unit[index].top = y;
          } else {
            console.warn('Unit element not found at index:', index);
          }
          break;
        case 'duties':
          // 增强职位元素的拖拽处理
          if (Array.isArray(this.form.duties) && this.form.duties[index]) {
            if (!this.form.duties[index].left && this.form.duties[index].left !== 0) {
              this.form.duties[index].left = 0;
            }
            if (!this.form.duties[index].top && this.form.duties[index].top !== 0) {
              this.form.duties[index].top = 0;
            }
            this.form.duties[index].left = x;
            this.form.duties[index].top = y;
          } else {
            console.warn('Duties element not found at index:', index);
          }
          break;
        case 'address':
          // 增强地址元素的拖拽处理
          if (Array.isArray(this.form.address) && this.form.address[index]) {
            if (!this.form.address[index].left && this.form.address[index].left !== 0) {
              this.form.address[index].left = 0;
            }
            if (!this.form.address[index].top && this.form.address[index].top !== 0) {
              this.form.address[index].top = 0;
            }
            this.form.address[index].left = x;
            this.form.address[index].top = y;
          } else {
            console.warn('Address element not found at index:', index);
          }
          break;
        case 'iconL':
          if (Array.isArray(this.form.iconL) && this.form.iconL[index]) {
            this.form.iconL[index].left = x;
            this.form.iconL[index].top = y;
          }
          break;
        default:
          console.warn('Unknown drag type:', type);
      }
    },
    // 打开上传
    openUpload(type, index) {
      this.uploadType = type;
      this.uploadIndex = index;
      this.isupload = true;
    },
    // 返回图片
    returnImgsFunc(e) {
      if (e && e.length > 0) {
        switch (this.uploadType) {
          case 1: // 背景图
            this.form.backdrop.src = e[0].file_path;
            // 这里可以添加获取图片尺寸的逻辑
            break;
          case 2: // Logo
            this.form.logo.src = e[0].file_path;
            break;
          case 3: // 图标
            if (!this.form.iconL[this.uploadIndex]) {
              this.form.iconL[this.uploadIndex] = {};
            }
            this.form.iconL[this.uploadIndex].src = e[0].file_path;
            break;
        }
      }
      this.isupload = false;
    },
    // 添加图标
    addIcon() {
      this.form.iconL.push({
        src: '',
        width: 30,
        height: 30,
        left: 100,
        top: 100
      });
      // 添加后重新初始化拖拽事件
      this.$nextTick(() => {
        this.dragEventArray('iconL');
      });
    },
    // 减少图标
    editIcon() {
      if (this.form.iconL.length > 0) {
        this.form.iconL.pop();
      }
    },
    // 添加公司
    addUnit() {
      const index = this.form.unit.length;
      this.form.unit.push({
        fontSize: 14,
        color: '#000000',
        left: 133,
        top: 167 + (index * 20),
        fontWeight: 100,
        type: 'text'
      });
      // 添加后重新初始化拖拽事件
      this.$nextTick(() => {
        this.dragEventArray('unit');
      });
    },
    // 减少公司
    editUnit() {
      if (this.form.unit.length > 0) {
        this.form.unit.pop();
      }
    },
    // 添加职位
    addDuties() {
      const index = this.form.duties.length;
      this.form.duties.push({
        fontSize: 14,
        color: '#000000',
        left: 260,
        top: 167 + (index * 20),
        fontWeight: 400,
        type: 'text'
      });
      // 添加后重新初始化拖拽事件
      this.$nextTick(() => {
        this.dragEventArray('duties');
      });
    },
    // 减少职位
    editDuties() {
      if (this.form.duties.length > 0) {
        this.form.duties.pop();
      }
    },
    // 添加地址
    addAddress() {
      const index = this.form.address.length;
      this.form.address.push({
        fontSize: 14,
        color: '#000000',
        left: 133,
        top: 206 + (index * 20),
        fontWeight: 400,
        type: 'text'
      });
      // 添加后重新初始化拖拽事件
      this.$nextTick(() => {
        this.dragEventArray('address');
      });
    },
    // 减少地址
    editAddress() {
      if (this.form.address.length > 0) {
        this.form.address.pop();
      }
    },
    // 提交表单
    onSubmit() {
      let self = this;
      self.$refs.form.validate(valid => {
        if (valid) {
          self.loading = true;
          // 确保必要字段存在
          if (!self.form.backdrop.src) {
            self.$message.error('请上传背景图');
            self.loading = false;
            return;
          }
          BusinessApi.templateEdit({
            template_id: self.template_id,
            template: self.form
          }, true)
            .then(res => {
              self.loading = false;
              self.$message({
                message: '保存成功',
                type: 'success'
              });
              self.$router.push('/plus/business/template/index');
            })
            .catch(error => {
              self.loading = false;
              self.$message.error('保存失败,请重试');
            });
        }
      });
    },
    // 返回
    back() {
      this.$router.push('/plus/business/template/index');
    }
  }
};
</script>
<style scoped>
.poster-box {
  display: flex;
  flex-wrap: wrap;
}
.poster-box .left-box {
  position: relative;
  overflow: hidden;
  margin: 0 30px;
  border: 1px solid #e0e0e0;
  border-radius: 8px;
  background-color: #f5f5f5;
}
.poster-box .left-box .img img {
  width: auto;
  object-fit: cover;
}
.poster-box .left-box .userinfo {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
}
.poster-box .left-box .pa {
  position: absolute;
  cursor: move;
}
.poster-box .left-box .photo,
.poster-box .left-box .logo,
.poster-box .left-box .icon {
  display: flex;
  justify-content: center;
  align-items: center;
  overflow: hidden;
  background: #ffffff;
  border: 1px solid #e0e0e0;
}
.poster-box .left-box .photo.radius,
.poster-box .left-box .logo.radius,
.poster-box .left-box .icon.radius {
  border-radius: 50%;
}
.poster-box .left-box .photo img,
.poster-box .left-box .logo img,
.poster-box .left-box .icon img {
  width: 100%;
  height: 100%;
  object-fit: contain;
}
.poster-box .left-box .name,
.poster-box .left-box .mobile,
.poster-box .left-box .unit,
.poster-box .left-box .duties,
.poster-box .left-box .address,
.poster-box .left-box .wechat,
.poster-box .left-box .mailbox,
.poster-box .left-box .phone {
  padding: 2px 5px;
  white-space: nowrap;
  border-radius: 4px;
}
.poster-box .right-box {
  flex: 1;
  min-width: 400px;
  max-height: 800px;
  overflow-y: auto;
  padding: 0 20px;
}
.tips {
  margin-top: 5px;
  color: #999;
  font-size: 12px;
}
.d-s-r {
  display: flex;
  align-items: center;
}
.max-w460 {
  max-width: 460px;
}
.max-w200 {
  max-width: 200px;
}
.mr-10 {
  margin-right: 10px;
}
.ml-10 {
  margin-left: 10px;
}
.icon-setting {
  border: 1px solid #e0e0e0;
  padding: 10px;
  margin-bottom: 10px;
  border-radius: 4px;
}
</style>
shop_vue/src/views/plus/business/template/index.vue
New file
@@ -0,0 +1,234 @@
<template>
  <!--
    作者:系统自动生成
    时间:当前日期
    描述:插件中心-名片模板-管理列表
  -->
  <div class="user" v-loading="listLoading">
    <div class="common-form">名片模板管理</div>
    <div class="common-main">
      <div class="common-toolbar">
        <el-button type="primary" @click="addTemplate">添加模板</el-button>
        <el-button type="danger" @click="batchDelete" :disabled="multipleSelection.length === 0">批量删除</el-button>
      </div>
      <el-table :data="listData" border class="common-table" @selection-change="handleSelectionChange">
        <el-table-column type="selection" width="55" align="center"></el-table-column>
        <el-table-column prop="template_id" label="ID" width="80" align="center"></el-table-column>
        <el-table-column prop="image" label="模板图片"  align="center">
          <template slot-scope="scope">
            <a :href="scope.row.image" target="_blank">
              <img :src="scope.row.image" class="logo-img" alt="模板图片">
            </a>
          </template>
        </el-table-column>
        <el-table-column prop="create_time" label="创建时间" width="180" align="center"></el-table-column>
        <el-table-column label="操作"  align="center" fixed="right">
          <template slot-scope="scope">
            <el-button type="primary" size="small" @click="editTemplate(scope.row)" class="m-r-5">编辑</el-button>
            <el-button type="danger" size="small" @click="deleteTemplate(scope.row)" class="m-r-5">删除</el-button>
          </template>
        </el-table-column>
      </el-table>
      <div class="common-pagination">
        <div class="page-text">共 {{ total }} 条记录</div>
        <el-pagination
          layout="prev, pager, next, jumper"
          :total="total"
          :page-size="listQuery.pagesize"
          :current-page="listQuery.page"
          @current-change="pageChange"
          @size-change="pageSizeChange"
        ></el-pagination>
      </div>
    </div>
  </div>
</template>
<script>
import BusinessApi from '@/api/business';
export default {
  data() {
    return {
      total: 0,
      listLoading: false,
      multipleSelection: [],
      listQuery: {
        page: 1,
        pagesize: 10,
        title: ''
      },
      listData: []
    };
  },
  created() {
    this.getList();
  },
  methods: {
    // 获取列表数据
    getList() {
      const that = this;
      that.listLoading = true;
      BusinessApi.templateList(that.listQuery)
        .then(res => {
          that.listLoading = false;
          that.listData = res.data.list.data;
          that.total = res.data.list.total;
        })
        .catch(error => {
          that.listLoading = false;
          console.error('获取列表失败', error);
          that.$message.error('获取数据失败,请重试');
        });
    },
    // 查询
    handleQuery() {
      this.listQuery.page = 1;
      this.getList();
    },
    // 重置
    resetQuery() {
      this.listQuery = {
        page: 1,
        pagesize: 10,
        title: ''
      };
      this.getList();
    },
    // 分页改变
    pageChange(page) {
      this.listQuery.page = page;
      this.getList();
    },
    // 每页条数改变
    pageSizeChange(pagesize) {
      this.listQuery.pagesize = pagesize;
      this.getList();
    },
    // 选择项变化
    handleSelectionChange(selection) {
      this.multipleSelection = selection;
    },
    // 批量删除
    batchDelete() {
      if (this.multipleSelection.length === 0) {
        this.$message.warning('请选择要删除的模板');
        return;
      }
      const template_ids = this.multipleSelection.map(item => item.template_id).join(',');
      this.$confirm('确定要删除选中的 ' + this.multipleSelection.length + ' 个模板吗?', '提示', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning'
      }).then(() => {
        this.listLoading = true;
        BusinessApi.templateDelete({template_id: template_ids})
          .then(res => {
            this.listLoading = false;
            this.$message({
              message: '删除成功',
              type: 'success'
            });
            this.getList();
            this.multipleSelection = [];
          })
          .catch(error => {
            this.listLoading = false;
            this.$message.error('删除失败,请重试');
          });
      });
    },
    // 添加模板
    addTemplate() {
      this.$router.push('/plus/business/template/add');
    },
    // 编辑模板
    editTemplate(row) {
      this.$router.push(`/plus/business/template/edit?template_id=${row.template_id}`);
    },
    // 删除模板
    deleteTemplate(row) {
      this.$confirm('确定要删除该模板吗?', '提示', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning'
      }).then(() => {
        this.listLoading = true;
        BusinessApi.templateDelete({template_id: row.template_id})
          .then(res => {
            this.listLoading = false;
            this.$message({
              message: '删除成功',
              type: 'success'
            });
            this.getList();
          })
          .catch(error => {
            this.listLoading = false;
            this.$message.error('删除失败,请重试');
          });
      });
    },
    // 更改默认状态
    changeStatus(row) {
      BusinessApi.templateDefault({template_id: row.template_id, is_default: row.is_default})
        .then(res => {
          this.$message({
            message: '设置成功',
            type: 'success'
          });
        })
        .catch(error => {
          this.$message.error('设置失败,请重试');
          this.getList(); // 刷新列表以恢复正确状态
        });
    }
  }
};
</script>
<style scoped>
.logo-img {
  height: 170px;
  border-radius: 4px;
}
.common-search {
  margin-bottom: 15px;
  padding: 10px 15px;
  background-color: #f5f7fa;
  border-radius: 4px;
}
.common-toolbar {
  margin-bottom: 15px;
  padding: 10px 0;
}
.common-table {
  margin-bottom: 15px;
}
.common-pagination {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 10px 0;
}
.page-text {
  color: #606266;
}
.inline-input {
  margin-right: 10px;
}
.m-r-5 {
  margin-right: 5px;
}
</style>