项目决策、报批报建 (#5)

This commit is contained in:
david 2019-05-09 11:30:22 +00:00 committed by Gitea
parent 2a3d261666
commit 06fdb33a48
303 changed files with 40589 additions and 294 deletions

29
.gitignore vendored
View File

@ -1,41 +1,20 @@
.DS_Store
/dist
# Editor directories and files
.idea
.vscode
# Byte-compiled / optimized / DLL files
*/__pycache__/
/xadmin/*/__pycache__
# Django files
db.sqlite3
#node.js files
/frontend/node_modules
/media/file/*
# custom
/Frontend/dist
/Frontend/node_modules
/ConstructionConsultationSystem/settings_prod.py
*/migrations/*
/media/*
/static/

View File

@ -1,3 +1,2 @@
from django.db import models
# Create your models here.

View File

@ -1,3 +1,7 @@
from django.test import TestCase
from ConstructionConsultationSystem.settings import BASE_DIR
import os
import time
print(time.time())
print(os.path.join(BASE_DIR, 'media','file','project_image'))
# Create your tests here.

View File

@ -15,7 +15,6 @@ import os
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/2.1/howto/deployment/checklist/
@ -25,8 +24,7 @@ SECRET_KEY = 'jh)1v3!d)ho3b1!thsy_h!&sf3-m%mhz)3!+r*38_)(ksew6k%'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = []
ALLOWED_HOSTS = ['*']
# Application definition
@ -37,9 +35,11 @@ INSTALLED_APPS = [
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'django_filters',
'rest_framework',
'xadmin',
'crispy_forms',
'rest_framework_swagger',
'User',
'ProjectInfoDisplay',
'DecisionPhase',
@ -47,12 +47,14 @@ INSTALLED_APPS = [
'CompletionPhase',
'VideoDisplay',
'SystemSettings',
'File',
]
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly'
]
# 'rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly'
],
'DEFAULT_FILTER_BACKENDS': ('django_filters.rest_framework.DjangoFilterBackend',)
}
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
@ -66,10 +68,12 @@ MIDDLEWARE = [
ROOT_URLCONF = 'ConstructionConsultationSystem.urls'
AUTH_USER_MODEL = 'User.User'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [os.path.join(BASE_DIR, 'templates')]
'DIRS': [os.path.join(BASE_DIR, 'Frontend', 'dist')]
,
'APP_DIRS': True,
'OPTIONS': {
@ -85,17 +89,28 @@ TEMPLATES = [
WSGI_APPLICATION = 'ConstructionConsultationSystem.wsgi.application'
# Database
# https://docs.djangoproject.com/en/2.1/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
'ENGINE': 'django.db.backends.postgresql_psycopg2',
'NAME': 'ccs',
'USER': 'postgres',
'PASSWORD': '199873abc',
'HOST': '47.101.206.221',
'PORT': '5432',
# 'CONN_MAX_AGE': 5,
}
}
# DATABASES = {
# 'default': {
# 'ENGINE': 'django.db.backends.sqlite3',
# 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
# }
# }
# Password validation
# https://docs.djangoproject.com/en/2.1/ref/settings/#auth-password-validators
@ -115,7 +130,6 @@ AUTH_PASSWORD_VALIDATORS = [
},
]
# Internationalization
# https://docs.djangoproject.com/en/2.1/topics/i18n/
@ -129,12 +143,17 @@ USE_L10N = True
USE_TZ = False
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/2.1/howto/static-files/
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media/')
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(BASE_DIR, "static")
STATICFILES_DIRS = [
os.path.join(BASE_DIR, 'static')]
os.path.join(BASE_DIR, 'Frontend', 'dist', 'static'),
]
# get sessionID in frontend
SESSION_COOKIE_HTTPONLY = False
LOGIN_URL = '/users/login/'
LOGOUT_URL = '/users/logout/'

View File

@ -1 +0,0 @@
from .settings import *

View File

@ -13,25 +13,30 @@ Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
import xadmin
from django.urls import path,include
from rest_framework import routers
from ProjectInfoDisplay import views
router = routers.DefaultRouter()
router.register(r'users', views.UserViewSet)
router.register(r'groups', views.GroupViewSet)
from django.urls import path, include, re_path
from rest_framework_swagger.views import get_swagger_view
schema_view = get_swagger_view(title="我的docs")
from django.views.static import serve
from . import settings
from django.views.generic.base import TemplateView
urlpatterns = [
path('', TemplateView.as_view(template_name='index.html')),
path('docs/', schema_view),
path('api/file/', include(('File.urls', 'File'), namespace='File')),
path('xadmin/', xadmin.site.urls),
re_path(r'media/(?P<path>.*)', serve, {'document_root': settings.MEDIA_ROOT}),
path('api-auth/', include('rest_framework.urls')),
path('',include(('User.urls','User'),namespace='User')),
path('pid/',include(('ProjectInfoDisplay.urls','ProjectInfoDisplay'),namespace='ProjectInfoDisplay')),
path('dp/',include(('DecisionPhase.urls','DecisionPhase'),namespace='DecisionPhase')),
path('ip/',include(('ImplementationPhase.urls','ImplementationPhase'),namespace='ImplementationPhase')),
path('cp/',include(('CompletionPhase.urls','CompletionPhase'),namespace='CompletionPhase')),
path('vd/',include(('VideoDisplay.urls','VideoDisplay'),namespace='VideoDisplay')),
path('ss/',include(('SystemSettings.urls','SystemSettings'),namespace='SystemSettings')),
path('api/', include(router.urls)),
path('api/user/', include(('User.urls', 'User'), namespace='User')),
path('api/project/', include(('ProjectInfoDisplay.urls', 'ProjectInfoDisplay'), namespace='ProjectInfoDisplay')),
path('api/decision/', include(('DecisionPhase.urls', 'DecisionPhase'), namespace='DecisionPhase')),
path('api/implement/',
include(('ImplementationPhase.urls', 'ImplementationPhase'), namespace='ImplementationPhase')),
path('api/completion/', include(('CompletionPhase.urls', 'CompletionPhase'), namespace='CompletionPhase')),
path('api/video/', include(('VideoDisplay.urls', 'VideoDisplay'), namespace='VideoDisplay')),
path('api/setting/', include(('SystemSettings.urls', 'SystemSettings'), namespace='SystemSettings')),
# path('api/account/', include(('SystemSettings.urls', 'SystemSettings'), namespace='SystemSettings')),
]

View File

@ -0,0 +1 @@
default_app_config = "DecisionPhase.apps.DecisionphaseConfig"

15
DecisionPhase/adminx.py Normal file
View File

@ -0,0 +1,15 @@
import xadmin
from . import models
class DecisionAdmin(object):
list_display = ['title', 'status', 'decision_id']
search_fields = ['title', 'status', 'decision_id']
list_filter = ['title', 'status', 'decision_id']
class DesicionMatterAdmin(object):
list_display = ['matter_id','matter']
search_fields = ['matter_id','matter']
list_filter = ['matter_id','matter']
xadmin.site.register(models.Decision,DecisionAdmin)
xadmin.site.register(models.DecisionMatter,DesicionMatterAdmin)

View File

@ -3,3 +3,4 @@ from django.apps import AppConfig
class DecisionphaseConfig(AppConfig):
name = 'DecisionPhase'
verbose_name = '项目决策阶段'

View File

@ -0,0 +1,52 @@
# Generated by Django 2.0.10 on 2019-04-22 21:00
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
('ProjectInfoDisplay', '0008_delete_file'),
('File', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='Decision',
fields=[
('decision_id', models.AutoField(primary_key=True, serialize=False, verbose_name='决策条目主键')),
('title', models.CharField(max_length=128, verbose_name='决策条目名称')),
('estimate', models.DecimalField(blank=True, decimal_places=2, default=0, help_text='限制其为小数decimal10.2数字代表最大位数eg.小数点前最大为7位', max_digits=12, null=True, verbose_name='预计花费')),
('actual', models.DecimalField(blank=True, decimal_places=2, help_text='限制其为小数decimal10.2数字代表最大位数eg.小数点前最大为7位', max_digits=12, null=True, verbose_name='实际花费')),
('status', models.CharField(choices=[('DONE', '已完成'), ('DOING', '正在办理'), ('CANNOT', '无法办理'), ('NEEDNOT', '不用办理'), ('WILLDO', '尚未办理')], default='尚未办理', max_length=8, verbose_name='状态')),
('project', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='ProjectInfoDisplay.Project', verbose_name='对应的工程项目')),
],
options={
'verbose_name': '项目决策阶段条目',
'verbose_name_plural': '项目决策阶段条目',
},
),
migrations.CreateModel(
name='DecisionMatter',
fields=[
('matter_id', models.AutoField(primary_key=True, serialize=False, verbose_name='办理事项主键')),
('matter', models.CharField(max_length=256, verbose_name='办理事项')),
('number', models.CharField(blank=True, help_text='可空可不填', max_length=256, null=True, verbose_name='份数')),
('department', models.CharField(blank=True, help_text='可空可不填', max_length=256, null=True, verbose_name='办理部门')),
('status', models.CharField(blank=True, help_text='可空可不填', max_length=256, null=True, verbose_name='状态')),
('telephone', models.CharField(blank=True, help_text='可空可不填', max_length=256, null=True, verbose_name='联系方式')),
('paid_status', models.CharField(blank=True, help_text='可空可不填', max_length=256, null=True, verbose_name='是否缴费')),
('deal_time', models.CharField(blank=True, help_text='可空可不填', max_length=256, null=True, verbose_name='办理时限')),
('note', models.CharField(blank=True, help_text='可空可不填', max_length=256, null=True, verbose_name='备注')),
('decision', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='DecisionPhase.Decision', verbose_name='对应的决策条目')),
('file', models.ManyToManyField(blank=True, to='File.File', verbose_name='所需文件')),
],
options={
'verbose_name': '项目决策阶段对应文件',
'verbose_name_plural': '项目决策阶段对应文件',
},
),
]

View File

@ -0,0 +1,19 @@
# Generated by Django 2.0.10 on 2019-04-22 21:20
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('DecisionPhase', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='decision',
name='project',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='ProjectInfoDisplay.Project', verbose_name='对应的工程项目'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 2.0.10 on 2019-04-22 21:22
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('DecisionPhase', '0002_auto_20190422_2120'),
]
operations = [
migrations.AlterField(
model_name='decision',
name='status',
field=models.CharField(choices=[('DONE', '已完成'), ('DOING', '正在办理'), ('CANNOT', '无法办理'), ('NEEDNOT', '不用办理'), ('WILLDO', '尚未办理')], default='WILLDO', max_length=8, verbose_name='状态'),
),
]

View File

@ -0,0 +1,19 @@
# Generated by Django 2.0.10 on 2019-04-22 21:43
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('DecisionPhase', '0003_auto_20190422_2122'),
]
operations = [
migrations.AlterField(
model_name='decisionmatter',
name='decision',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='DecisionPhase.Decision', verbose_name='对应的决策条目'),
),
]

View File

@ -0,0 +1,17 @@
# Generated by Django 2.0.10 on 2019-05-07 15:48
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('DecisionPhase', '0004_auto_20190422_2143'),
]
operations = [
migrations.AlterModelOptions(
name='decisionmatter',
options={'verbose_name': '项目决策阶段对应办理事项', 'verbose_name_plural': '项目决策阶段对应办理事项'},
),
]

View File

@ -1,3 +1,60 @@
from django.db import models
from ProjectInfoDisplay.models import Project
from File.models import File
# Create your models here.
class Decision(models.Model):
STATUS_CHPICES = (
("DONE","已完成"),
("DOING","正在办理"),
("CANNOT","无法办理"),
("NEEDNOT","不用办理"),
("WILLDO","尚未办理"),
)
decision_id = models.AutoField(verbose_name="决策条目主键",primary_key=True)
project = models.ForeignKey(
Project,
verbose_name="对应的工程项目",
on_delete=models.CASCADE
)
title = models.CharField(verbose_name="决策条目名称",max_length=128)
estimate = models.DecimalField(verbose_name="预计花费",max_digits=12, decimal_places=2, null=True, blank=True,default=0
, help_text="限制其为小数decimal10.2数字代表最大位数eg.小数点前最大为7位")
actual = models.DecimalField(verbose_name="实际花费", max_digits=12, decimal_places=2, null=True, blank=True
, help_text="限制其为小数decimal10.2数字代表最大位数eg.小数点前最大为7位")
status = models.CharField(verbose_name="状态",choices=STATUS_CHPICES,
max_length=8,default="WILLDO")
class Meta:
verbose_name = "项目决策阶段条目"
verbose_name_plural=verbose_name
def __str__(self):
return self.title
class DecisionMatter(models.Model):
matter_id = models.AutoField(verbose_name="办理事项主键",primary_key=True)
decision = models.ForeignKey(
Decision,
verbose_name="对应的决策条目",
on_delete=models.CASCADE
)
matter = models.CharField(verbose_name="办理事项",max_length=256)
number = models.CharField(verbose_name="份数",max_length=256,null=True,blank=True,help_text="可空可不填")
department = models.CharField(verbose_name="办理部门",max_length=256,null=True,blank=True,help_text="可空可不填")
status = models.CharField(verbose_name="状态",max_length=256,null=True,blank=True,help_text="可空可不填")
telephone = models.CharField(verbose_name="联系方式",max_length=256,null=True,blank=True,help_text="可空可不填")
paid_status = models.CharField(verbose_name="是否缴费",max_length=256,null=True,blank=True,help_text="可空可不填")
deal_time = models.CharField(verbose_name="办理时限",max_length=256,null=True,blank=True,help_text="可空可不填")
note = models.CharField(verbose_name="备注",max_length=256,null=True,blank=True,help_text="可空可不填")
file = models.ManyToManyField(
File,
verbose_name="所需文件",
blank=True
)
class Meta:
verbose_name = "项目决策阶段对应办理事项"
verbose_name_plural = verbose_name
def __str__(self):
return self.matter

View File

@ -1,14 +1,17 @@
from django.contrib.auth.models import User, Group
from . import models
from rest_framework import serializers
class UserSerializer(serializers.HyperlinkedModelSerializer):
class DecisionSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ('url', 'username', 'email', 'groups')
model = models.Decision
exclude = ['project']
read_only_fields = ['decision_id','title','estimate',]
class DecisionMatterSerializer(serializers.ModelSerializer):
class Meta:
model = models.DecisionMatter
fields = '__all__'
read_only_fields = ['matter_id']
class GroupSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Group
fields = ('url', 'name')

View File

@ -1,5 +1,9 @@
from django.urls import path,include
from . import views
urlpatterns = [
# path('',views.ProjectInfoDispalyView.as_view(),name='ProjectInfoDisplay')
]
path('',views.DecisionAPIView.as_view(),name='Decision'),
path('matter/', views.DecisionMatterAPIView.as_view(), name='DecisionMatter'),
path('matter/<str:matter_id>/', views.DecisionMatterDetailAPIView.as_view(), name='DecisionMatterDetail'),
path('<str:decision_id>/',views.DecisionDetailAPIView.as_view(),name='DecisionDetail'),
]

View File

@ -1,3 +1,34 @@
from django.shortcuts import render
from rest_framework import generics
from . import models
from . import serializers
from rest_framework.permissions import IsAuthenticated
class DecisionAPIView(generics.ListAPIView):
queryset = models.Decision.objects.all().order_by('decision_id')
serializer_class = serializers.DecisionSerializer
permissions = (IsAuthenticated,)
# Create your views here.
def get_queryset(self):
return self.queryset.filter(project = self.request.GET.get('project_id'))
class DecisionDetailAPIView(generics.RetrieveUpdateAPIView):
queryset = models.Decision.objects.all()
serializer_class = serializers.DecisionSerializer
permission_classes = (IsAuthenticated,)
lookup_field = 'decision_id'
class DecisionMatterAPIView(generics.ListCreateAPIView):
queryset = models.DecisionMatter.objects.all()
serializer_class = serializers.DecisionMatterSerializer
permission_classes = (IsAuthenticated,)
def get_queryset(self):
return self.queryset.filter(decision = self.request.GET.get('decision_id'))
def perform_create(self, serializer):
serializer.save(decision=models.Decision.objects.get(
decision_id=self.request.GET.get('decision_id')))
class DecisionMatterDetailAPIView(generics.RetrieveUpdateDestroyAPIView):
queryset = models.DecisionMatter.objects.all()
serializer_class = serializers.DecisionMatterSerializer
permission_classes = (IsAuthenticated,)
lookup_field = 'matter_id'

59
Dockerfile Normal file
View File

@ -0,0 +1,59 @@
##########################################################################
# DokcerFile to build ConstructionConsoleSystem container images
# Based on alpine
##########################################################################
# Set the base image to Ubuntu
FROM python:3.7-alpine
# File Authot / Maintainer
LABEL AUTHOR="T-hugh"
RUN mkdir /ConstructionConsoleSystem/
WORKDIR /ConstructionConsoleSystem/
RUN pip install --upgrade pip
# 设置alpine的镜像地址为阿里云的地址并更新pip源
RUN echo "https://mirrors.aliyun.com/alpine/v3.9/main/" > /etc/apk/repositories \
&& pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple
# 更新安装 bash curl python3等工具
RUN apk update \
&& apk add --no-cache bash
# 安装node\npm\更新npm源
RUN apk add --no-cache nodejs\
&& apk add --no-cache npm\
&& npm config set registry https://registry.npm.taobao.org
# && echo "DOCKER_OPTS=\"$DOCKER_OPTS --registry-mirror=http://xxx.m.daocloud.io\"" > /etc/default/docker
RUN apk add --no-cache openjpeg
# 一开始从哪来?
COPY ./Pipfile /ConstructionConsoleSystem/Pipfile
COPY ./Pipfile.lock /ConstructionConsoleSystem/Pipfile.lock
# 装pipenv与依赖
RUN pip3 install pipenv \
&& pipenv install
WORKDIR /ConstructionConsoleSystem/Frontend
RUN npm install
WORKDIR /ConstructionConsoleSystem/
RUN apk add -U --no-cache gcc linux-headers musl-dev libc-dev libuuid\
&& pipenv install uwsgi \
&& apk del gcc linux-headers musl-dev libc-dev
# 暴露的端口
EXPOSE 8000
# 运行服务的命令
CMD pipenv run uwsgi --ini /config/uwsgi.ini
# 更新镜像用于优化体积
# # 此环境专用做运行django项目因此移除不必要的工具减少空间
# RUN python3 -m pip uninstall -y pip setuptools wheel \
# && apk del curl

1
File/__init__.py Normal file
View File

@ -0,0 +1 @@
default_app_config = "File.apps.FileConfig"

3
File/admin.py Normal file
View File

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

10
File/adminx.py Normal file
View File

@ -0,0 +1,10 @@
import xadmin
from xadmin import views
from .models import File
class FileAdmin(object):
list_display = ['name']
search_fields = ['name']
list_filter = ['name']
xadmin.site.register(File,FileAdmin)

7
File/apps.py Normal file
View File

@ -0,0 +1,7 @@
from django.apps import AppConfig
class FileConfig(AppConfig):
name = 'File'
verbose_name = '文件'

View File

@ -0,0 +1,28 @@
# Generated by Django 2.0.10 on 2019-04-22 15:06
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='File',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('uuid', models.UUIDField(blank=True, default=None, help_text='uuid 由后台产生并返回', null=True, verbose_name='file uuid')),
('file', models.FileField(help_text='上传的文件', max_length=1024, upload_to='file/project_image/', verbose_name='文件')),
('name', models.CharField(help_text='文件名限制为130个字以内', max_length=512, verbose_name='文件名称')),
],
options={
'verbose_name': '系统文件',
'verbose_name_plural': '系统文件',
'ordering': ['id'],
},
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 2.0.10 on 2019-04-23 21:32
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('File', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='file',
name='file',
field=models.CharField(help_text='上传的文件路径', max_length=1024, verbose_name='文件'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 2.0.10 on 2019-04-23 21:38
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('File', '0002_auto_20190423_2132'),
]
operations = [
migrations.AlterField(
model_name='file',
name='file',
field=models.FileField(help_text='上传的文件路径', max_length=1024, upload_to='', verbose_name='文件'),
),
]

View File

15
File/models.py Normal file
View File

@ -0,0 +1,15 @@
from django.db import models
class File(models.Model):
uuid = models.UUIDField(verbose_name="file uuid", default=None, blank=True, null=True
, help_text="uuid 由后台产生并返回")
file = models.FileField(verbose_name="文件",
help_text="上传的文件路径", max_length=1024)
name = models.CharField(verbose_name="文件名称", max_length=512,
help_text="文件名限制为130个字以内")
class Meta:
verbose_name = '系统文件'
ordering = ['id']
verbose_name_plural = verbose_name

3
File/tests.py Normal file
View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

5
File/urls.py Normal file
View File

@ -0,0 +1,5 @@
from django.urls import path,include
from . import views
urlpatterns = [
path('',views.UploadFile.as_view(),name='file'),
]

38
File/views.py Normal file
View File

@ -0,0 +1,38 @@
from django.shortcuts import render
from .models import File
from django.views.generic.base import View
from django.shortcuts import HttpResponse
from django.contrib.auth.mixins import LoginRequiredMixin
import datetime
import os
from ConstructionConsultationSystem.settings import BASE_DIR
import uuid
import time
from rest_framework.views import APIView
from rest_framework.response import Response
class UploadFile(LoginRequiredMixin,APIView):
def post(self,request):
try:
image = request.FILES.get('image','')
image_name = image.name
name = str(time.time()) + "_" + image_name
path = os.path.join(BASE_DIR, 'media','file', name)
f = open(os.path.join(BASE_DIR, 'media','file', name), "wb")
for chunk in image.chunks():
f.write(chunk)
f.close()
File.objects.create(uuid=uuid.uuid3(uuid.NAMESPACE_DNS,name),
name=image_name,file=path)
return Response({
'uuid': uuid.uuid3(uuid.NAMESPACE_DNS,name),
'url' : f'{request.stream.scheme}://{request.stream.environ["HTTP_HOST"]}/media/file/{name}'
})
except Exception as e:
return Response({
"error":f"上传文件出错!+{e}"
})
def get(self,request):
pass

39
Frontend/.editorconfig Normal file
View File

@ -0,0 +1,39 @@
[*]
charset=utf-8
end_of_line=crlf
insert_final_newline=false
indent_style=space
indent_size=2
[{*.ng,*.sht,*.html,*.shtm,*.shtml,*.htm}]
indent_style=space
indent_size=2
[{*.jhm,*.xslt,*.xul,*.rng,*.xsl,*.xsd,*.ant,*.tld,*.fxml,*.jrxml,*.xml,*.jnlp,*.wsdl}]
indent_style=space
indent_size=2
[{.babelrc,.stylelintrc,jest.config,.eslintrc,.prettierrc,*.json,*.jsb3,*.jsb2,*.bowerrc}]
indent_style=space
indent_size=2
[*.svg]
indent_style=space
indent_size=2
[*.js.map]
indent_style=space
indent_size=2
[*.less]
indent_style=space
indent_size=2
[*.vue]
indent_style=space
indent_size=2
[{.analysis_options,*.yml,*.yaml}]
indent_style=space
indent_size=2

5
Frontend/.prettierrc Normal file
View File

@ -0,0 +1,5 @@
{
"printWidth": 120,
"semi": false,
"singleQuote": true
}

13
Frontend/babel.config.js Normal file
View File

@ -0,0 +1,13 @@
module.exports = {
presets: [
'@vue/app'
]
// ,
// plugins: [
// [ 'import', {
// 'libraryName': 'ant-design-vue',
// 'libraryDirectory': 'es',
// 'style': true // `style: true` 会加载 less 文件
// } ]
// ]
}

11
Frontend/jsconfig.json Normal file
View File

@ -0,0 +1,11 @@
{
"compilerOptions": {
"target": "es6",
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
},
"exclude": ["node_modules", "dist"],
"include": ["src/**/*"]
}

16830
Frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

123
Frontend/package.json Normal file
View File

@ -0,0 +1,123 @@
{
"name": "ConstructionConsultationSystem",
"version": "0.0.1",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint",
"test:unit": "vue-cli-service test:unit"
},
"dependencies": {
"@antv/data-set": "^0.10.1",
"ant-design-vue": "~1.3.2",
"axios": "^0.18.0",
"core-js": "^2.6.5",
"enquire.js": "^2.1.6",
"js-cookie": "^2.2.0",
"lodash": "^4.17.11",
"lodash.get": "^4.4.2",
"lodash.pick": "^4.4.0",
"md5": "^2.2.1",
"moment": "^2.24.0",
"nprogress": "^0.2.0",
"viser-vue": "^2.3.3",
"vue": "^2.5.22",
"vue-clipboard2": "^0.2.1",
"vue-cropper": "^0.4.4",
"vue-ls": "^3.2.0",
"vue-router": "^3.0.1",
"vue-schart": "^1.0.0",
"vuex": "^3.1.0",
"whatwg-fetch": "^3.0.0"
},
"devDependencies": {
"@vue/cli-plugin-babel": "^3.3.0",
"@vue/cli-plugin-eslint": "^3.3.0",
"@vue/cli-plugin-unit-jest": "^3.3.0",
"@vue/cli-service": "^3.5.3",
"@vue/eslint-config-standard": "^4.0.0",
"@vue/test-utils": "^1.0.0-beta.20",
"babel-core": "7.0.0-bridge.0",
"babel-eslint": "^10.0.1",
"babel-jest": "^23.6.0",
"babel-plugin-import": "^1.11.0",
"eslint": "^5.8.0",
"eslint-plugin-html": "^5.0.0",
"eslint-plugin-vue": "^5.0.0",
"less": "^3.8.1",
"less-loader": "^4.1.0",
"vue-template-compiler": "^2.5.22"
},
"eslintConfig": {
"root": true,
"env": {
"node": true
},
"extends": [
"plugin:vue/strongly-recommended",
"@vue/standard"
],
"parserOptions": {
"parser": "babel-eslint"
},
"rules": {
"generator-star-spacing": "off",
"no-mixed-operators": 0,
"vue/max-attributes-per-line": [
2,
{
"singleline": 5,
"multiline": {
"max": 1,
"allowFirstLine": false
}
}
],
"vue/attribute-hyphenation": 0,
"vue/html-self-closing": 0,
"vue/component-name-in-template-casing": 0,
"vue/html-closing-bracket-spacing": 0,
"vue/singleline-html-element-content-newline": 0,
"vue/no-unused-components": 0,
"vue/multiline-html-element-content-newline": 0,
"vue/no-use-v-if-with-v-for": 0,
"vue/html-closing-bracket-newline": 0,
"vue/no-parsing-error": 0,
"no-console": 0,
"no-tabs": 0,
"quotes": [
2,
"single",
{
"avoidEscape": true,
"allowTemplateLiterals": true
}
],
"semi": [
2,
"never",
{
"beforeStatementContinuationChars": "never"
}
],
"no-delete-var": 2,
"prefer-const": [
2,
{
"ignoreReadBeforeAssign": false
}
]
}
},
"postcss": {
"plugins": {
"autoprefixer": {}
}
},
"browserslist": [
"> 1%",
"last 2 versions",
"not ie <= 8"
]
}

7684
Frontend/public/color.less Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="zh-cmn-Hans">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>logo.png">
<title>建筑咨询管理系统</title>
<style>#preloadingAnimation{position:fixed;left:0;top:0;height:100%;width:100%;background:#ffffff;user-select:none;z-index: 9999;overflow: hidden}.lds-roller{display:inline-block;position:relative;left:50%;top:50%;transform:translate(-50%,-50%);width:64px;height:64px;}.lds-roller div{animation:lds-roller 1.2s cubic-bezier(0.5,0,0.5,1) infinite;transform-origin:32px 32px;}.lds-roller div:after{content:" ";display:block;position:absolute;width:6px;height:6px;border-radius:50%;background:#13c2c2;margin:-3px 0 0 -3px;}.lds-roller div:nth-child(1){animation-delay:-0.036s;}.lds-roller div:nth-child(1):after{top:50px;left:50px;}.lds-roller div:nth-child(2){animation-delay:-0.072s;}.lds-roller div:nth-child(2):after{top:54px;left:45px;}.lds-roller div:nth-child(3){animation-delay:-0.108s;}.lds-roller div:nth-child(3):after{top:57px;left:39px;}.lds-roller div:nth-child(4){animation-delay:-0.144s;}.lds-roller div:nth-child(4):after{top:58px;left:32px;}.lds-roller div:nth-child(5){animation-delay:-0.18s;}.lds-roller div:nth-child(5):after{top:57px;left:25px;}.lds-roller div:nth-child(6){animation-delay:-0.216s;}.lds-roller div:nth-child(6):after{top:54px;left:19px;}.lds-roller div:nth-child(7){animation-delay:-0.252s;}.lds-roller div:nth-child(7):after{top:50px;left:14px;}.lds-roller div:nth-child(8){animation-delay:-0.288s;}.lds-roller div:nth-child(8):after{top:45px;left:10px;}#preloadingAnimation .load-tips{color: #13c2c2;font-size:2rem;position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);margin-top:80px;text-align:center;width:400px;height:64px;} @keyframes lds-roller{0%{transform:rotate(0deg);} 100%{transform:rotate(360deg);}}</style>
</head>
<body>
<noscript>
<strong>We're sorry but vue-antd-pro doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app">
<div id="preloadingAnimation"><div class=lds-roller><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div></div><div class=load-tips>Loading</div></div>
</div>
<!-- built files will be auto injected -->
</body>
</html>

View File

@ -0,0 +1 @@
#preloadingAnimation{position:fixed;left:0;top:0;height:100%;width:100%;background:#ffffff;user-select:none;z-index: 9999;overflow: hidden}.lds-roller{display:inline-block;position:relative;left:50%;top:50%;transform:translate(-50%,-50%);width:64px;height:64px;}.lds-roller div{animation:lds-roller 1.2s cubic-bezier(0.5,0,0.5,1) infinite;transform-origin:32px 32px;}.lds-roller div:after{content:" ";display:block;position:absolute;width:6px;height:6px;border-radius:50%;background:#13c2c2;margin:-3px 0 0 -3px;}.lds-roller div:nth-child(1){animation-delay:-0.036s;}.lds-roller div:nth-child(1):after{top:50px;left:50px;}.lds-roller div:nth-child(2){animation-delay:-0.072s;}.lds-roller div:nth-child(2):after{top:54px;left:45px;}.lds-roller div:nth-child(3){animation-delay:-0.108s;}.lds-roller div:nth-child(3):after{top:57px;left:39px;}.lds-roller div:nth-child(4){animation-delay:-0.144s;}.lds-roller div:nth-child(4):after{top:58px;left:32px;}.lds-roller div:nth-child(5){animation-delay:-0.18s;}.lds-roller div:nth-child(5):after{top:57px;left:25px;}.lds-roller div:nth-child(6){animation-delay:-0.216s;}.lds-roller div:nth-child(6):after{top:54px;left:19px;}.lds-roller div:nth-child(7){animation-delay:-0.252s;}.lds-roller div:nth-child(7):after{top:50px;left:14px;}.lds-roller div:nth-child(8){animation-delay:-0.288s;}.lds-roller div:nth-child(8):after{top:45px;left:10px;}#preloadingAnimation .load-tips{color: #13c2c2;font-size:2rem;position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);margin-top:80px;text-align:center;width:400px;height:64px;} @keyframes lds-roller{0%{transform:rotate(0deg);} 100%{transform:rotate(360deg);}}

View File

@ -0,0 +1 @@
<div id="preloadingAnimation"><div class=lds-roller><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div></div><div class=load-tips>Loading</div></div>

View File

@ -0,0 +1,5 @@
<div class="preloading-animate">
<div class="preloading-wrapper">
<svg class="preloading-balls" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid"><circle cx="67.802" cy="59.907" r="6" fill="#51CACC"><animate attributeName="cx" values="75;57.72542485937369" keyTimes="0;1" dur="1s" repeatCount="indefinite"/><animate attributeName="cy" values="50;73.77641290737884" keyTimes="0;1" dur="1s" repeatCount="indefinite"/><animate attributeName="fill" values="#51CACC;#9DF871" keyTimes="0;1" dur="1s" repeatCount="indefinite"/></circle><circle cx="46.079" cy="69.992" r="6" fill="#9DF871"><animate attributeName="cx" values="57.72542485937369;29.774575140626318" keyTimes="0;1" dur="1s" repeatCount="indefinite"/><animate attributeName="cy" values="73.77641290737884;64.69463130731182" keyTimes="0;1" dur="1s" repeatCount="indefinite"/><animate attributeName="fill" values="#9DF871;#E0FF77" keyTimes="0;1" dur="1s" repeatCount="indefinite"/></circle><circle cx="29.775" cy="52.449" r="6" fill="#E0FF77"><animate attributeName="cx" values="29.774575140626318;29.774575140626315" keyTimes="0;1" dur="1s" repeatCount="indefinite"/><animate attributeName="cy" values="64.69463130731182;35.30536869268818" keyTimes="0;1" dur="1s" repeatCount="indefinite"/><animate attributeName="fill" values="#E0FF77;#DE9DD6" keyTimes="0;1" dur="1s" repeatCount="indefinite"/></circle><circle cx="41.421" cy="31.521" r="6" fill="#DE9DD6"><animate attributeName="cx" values="29.774575140626315;57.72542485937368" keyTimes="0;1" dur="1s" repeatCount="indefinite"/><animate attributeName="cy" values="35.30536869268818;26.22358709262116" keyTimes="0;1" dur="1s" repeatCount="indefinite"/><animate attributeName="fill" values="#DE9DD6;#FF708E" keyTimes="0;1" dur="1s" repeatCount="indefinite"/></circle><circle cx="64.923" cy="36.13" r="6" fill="#FF708E"><animate attributeName="cx" values="57.72542485937368;75" keyTimes="0;1" dur="1s" repeatCount="indefinite"/><animate attributeName="cy" values="26.22358709262116;49.99999999999999" keyTimes="0;1" dur="1s" repeatCount="indefinite"/><animate attributeName="fill" values="#FF708E;#51CACC" keyTimes="0;1" dur="1s" repeatCount="indefinite"/></circle></svg>
</div>
</div>

View File

@ -0,0 +1 @@
.preloading-animate{background:#ffffff;width:100%;height:100%;position:fixed;left:0;top:0;z-index:299;}.preloading-animate .preloading-wrapper{position:absolute;width:5rem;height:5rem;left:50%;top:50%;transform:translate(-50%,-50%);}.preloading-animate .preloading-wrapper .preloading-balls{font-size:5rem;}

View File

@ -0,0 +1 @@
<svg class="preloading-balls" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid"><circle cx="67.802" cy="59.907" r="6" fill="#51CACC"><animate attributeName="cx" values="75;57.72542485937369" keyTimes="0;1" dur="1s" repeatCount="indefinite"/><animate attributeName="cy" values="50;73.77641290737884" keyTimes="0;1" dur="1s" repeatCount="indefinite"/><animate attributeName="fill" values="#51CACC;#9DF871" keyTimes="0;1" dur="1s" repeatCount="indefinite"/></circle><circle cx="46.079" cy="69.992" r="6" fill="#9DF871"><animate attributeName="cx" values="57.72542485937369;29.774575140626318" keyTimes="0;1" dur="1s" repeatCount="indefinite"/><animate attributeName="cy" values="73.77641290737884;64.69463130731182" keyTimes="0;1" dur="1s" repeatCount="indefinite"/><animate attributeName="fill" values="#9DF871;#E0FF77" keyTimes="0;1" dur="1s" repeatCount="indefinite"/></circle><circle cx="29.775" cy="52.449" r="6" fill="#E0FF77"><animate attributeName="cx" values="29.774575140626318;29.774575140626315" keyTimes="0;1" dur="1s" repeatCount="indefinite"/><animate attributeName="cy" values="64.69463130731182;35.30536869268818" keyTimes="0;1" dur="1s" repeatCount="indefinite"/><animate attributeName="fill" values="#E0FF77;#DE9DD6" keyTimes="0;1" dur="1s" repeatCount="indefinite"/></circle><circle cx="41.421" cy="31.521" r="6" fill="#DE9DD6"><animate attributeName="cx" values="29.774575140626315;57.72542485937368" keyTimes="0;1" dur="1s" repeatCount="indefinite"/><animate attributeName="cy" values="35.30536869268818;26.22358709262116" keyTimes="0;1" dur="1s" repeatCount="indefinite"/><animate attributeName="fill" values="#DE9DD6;#FF708E" keyTimes="0;1" dur="1s" repeatCount="indefinite"/></circle><circle cx="64.923" cy="36.13" r="6" fill="#FF708E"><animate attributeName="cx" values="57.72542485937368;75" keyTimes="0;1" dur="1s" repeatCount="indefinite"/><animate attributeName="cy" values="26.22358709262116;49.99999999999999" keyTimes="0;1" dur="1s" repeatCount="indefinite"/><animate attributeName="fill" values="#FF708E;#51CACC" keyTimes="0;1" dur="1s" repeatCount="indefinite"/></circle></svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
Frontend/public/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

45
Frontend/src/App.vue Normal file
View File

@ -0,0 +1,45 @@
<template>
<a-locale-provider :locale="locale">
<div id="app">
<router-view/>
</div>
</a-locale-provider>
</template>
<script>
import zhCN from 'ant-design-vue/lib/locale-provider/zh_CN'
import { deviceEnquire, DEVICE_TYPE } from '@/utils/device'
export default {
data () {
return {
locale: zhCN
}
},
mounted () {
const { $store } = this
deviceEnquire(deviceType => {
switch (deviceType) {
case DEVICE_TYPE.DESKTOP:
$store.commit('TOGGLE_DEVICE', 'desktop')
$store.dispatch('setSidebar', true)
break
case DEVICE_TYPE.TABLET:
$store.commit('TOGGLE_DEVICE', 'tablet')
$store.dispatch('setSidebar', false)
break
case DEVICE_TYPE.MOBILE:
default:
$store.commit('TOGGLE_DEVICE', 'mobile')
$store.dispatch('setSidebar', true)
break
}
})
}
}
</script>
<style>
#app {
height: 100%;
}
</style>

View File

View File

View File

@ -0,0 +1,7 @@
import fetchAPI from '../utils/fetch'
export default {
getProjectDetails (id) {
return fetchAPI('project/'+id, 'get')
}
}

View File

@ -0,0 +1,29 @@
import fetchAPI from '../utils/fetch'
export default {
getDecisionList (project_id) {
return fetchAPI('decision/', 'get', null,{ project_id })
},
getDecisionSecond(decision_id){
return fetchAPI(`decision/${decision_id}/`, 'get', null)
},
updateDecisionSecond(decision){
return fetchAPI(`decision/${decision.decision_id}/`, 'put',decision)
},
getDecisionDetail(params){
return fetchAPI(`decision/matter/`, 'get', null,params)
},
createDecisionDetail(decisionDetail,params){
return fetchAPI(`decision/matter/`, 'post',decisionDetail,params)
},
updateDecisionDetail(decisionDetail,matterid){
return fetchAPI(`decision/matter/+${matterid}/`, 'put',decisionDetail)
},
deleteDecisionDetail(matterid){
return fetchAPI(`decision/matter/+${matterid}/`, 'delete')
},
getDecision (decision_id) {
return fetchAPI(`decision/${decision_id}/`, 'get')
}
}

View File

@ -0,0 +1,25 @@
import fetchAPI from '../utils/fetch'
export default {
getProjectDetail (id) {
return fetchAPI(`project/${id}/`, 'get')
},
updateProject (project) {
return fetchAPI(`project/${project.id}/`, 'put', project)
},
getAddressList (params) {
return fetchAPI(`project/addressList/`, 'get', null, params)
},
createAddressList (data,params) {
return fetchAPI('project/addressList/', 'post', data,params)
},
updateAddressList (data) {
return fetchAPI(`project/addressList/${data.id}/`, 'put', data)
},
deleteAddressListDetail(id){
return fetchAPI('project/addressList/'+id, 'delete')
},
getTypeList () {
return fetchAPI('project/companyType/', 'get')
}
}

0
Frontend/src/api/help.js Normal file
View File

View File

@ -0,0 +1,34 @@
import fetchAPI from '../utils/fetch'
export default {
getApplicationList (project_id) {
return fetchAPI('implement/application/', 'get', null, { project_id })
},
createApplication (project_id, data) {
return fetchAPI('implement/application/', 'post', data, { project_id })
},
updateApplication (application_id, data) {
return fetchAPI(`implement/application/${application_id}/`, 'put', data)
},
deleteApplication (application_id) {
return fetchAPI(`implement/application/${application_id}/`, 'delete')
},
getApplication (application_id) {
return fetchAPI(`implement/application/${application_id}/`, 'get')
},
createApplicationMatter (application_id, data) {
return fetchAPI('implement/application/matter/', 'post', data, { application_id })
},
getApplicationMatterList (application_id) {
return fetchAPI('implement/application/matter/', 'get', null, { application_id })
},
getApplicationMatter (matter_id) {
return fetchAPI(`implement/application/matter/${matter_id}/`, 'get')
},
updateApplicationMatter (matter_id, data) {
return fetchAPI(`implement/application/matter/${matter_id}/`, 'put', data)
},
deleteApplicationMatter (matter_id) {
return fetchAPI(`implement/application/matter/${matter_id}/`, 'delete')
}
}

12
Frontend/src/api/index.js Normal file
View File

@ -0,0 +1,12 @@
const api = {
Login: '/auth/login',
Logout: '/auth/logout',
ForgePassword: '/auth/forge-password',
Register: '/auth/register',
twoStepCode: '/auth/2step-code',
SendSms: '/account/sms',
SendSmsErr: '/account/sms_err',
// get my info
UserInfo: '/user/info'
}
export default api

61
Frontend/src/api/login.js Normal file
View File

@ -0,0 +1,61 @@
import api from './index'
import { axios } from '@/utils/request'
/**
* login func
* parameter: {
* username: '',
* password: '',
* remember_me: true,
* captcha: '12345'
* }
* @param parameter
* @returns {*}
*/
export function login (parameter) {
return axios({
url: '/auth/login',
method: 'post',
data: parameter
})
}
export function getSmsCaptcha (parameter) {
return axios({
url: api.SendSms,
method: 'post',
data: parameter
})
}
export function getInfo () {
return axios({
url: '/user/info',
method: 'get',
headers: {
'Content-Type': 'application/json;charset=UTF-8'
}
})
}
export function logout () {
return axios({
url: '/auth/logout',
method: 'post',
headers: {
'Content-Type': 'application/json;charset=UTF-8'
}
})
}
/**
* get user 2step code open?
* @param parameter {*}
*/
export function get2step (parameter) {
return axios({
url: api.twoStepCode,
method: 'post',
data: parameter
})
}

View File

@ -0,0 +1,62 @@
import { axios } from '@/utils/request'
const api = {
user: '/user',
role: '/role',
service: '/service',
permission: '/permission',
permissionNoPager: '/permission/no-pager',
orgTree: '/org/tree'
}
export default api
export function getUserList (parameter) {
return axios({
url: api.user,
method: 'get',
params: parameter
})
}
export function getRoleList (parameter) {
return axios({
url: api.role,
method: 'get',
params: parameter
})
}
export function getServiceList (parameter) {
return axios({
url: api.service,
method: 'get',
params: parameter
})
}
export function getPermissions (parameter) {
return axios({
url: api.permissionNoPager,
method: 'get',
params: parameter
})
}
export function getOrgTree (parameter) {
return axios({
url: api.orgTree,
method: 'get',
params: parameter
})
}
// id == 0 add post
// id != 0 update put
export function saveService (parameter) {
return axios({
url: api.service,
method: parameter.id === 0 ? 'post' : 'put',
data: parameter
})
}

View File

@ -0,0 +1,28 @@
import fetchAPI from '../utils/fetch'
export default {
getProjectList (params) {
return fetchAPI('project/', 'get', null, params)
},
getScaleList () {
return fetchAPI('project/scale/', 'get')
},
getPropertyList () {
return fetchAPI('project/property/', 'get')
},
getPurposeList () {
return fetchAPI('project/purpose/', 'get')
},
getFundSourcesList () {
return fetchAPI('project/fundSources/', 'get')
},
getStyleList () {
return fetchAPI('project/style/', 'get')
},
createProject (project) {
return fetchAPI('project/', 'post', project)
},
validateID (id) {
return fetchAPI('project/checkID/', 'post', { id })
}
}

View File

View File

16
Frontend/src/api/user.js Normal file
View File

@ -0,0 +1,16 @@
import fetchAPI from '../utils/fetch'
export default {
login (username, password) {
return fetchAPI('user/login/', 'post', {
username,
password
})
},
logout () {
return fetchAPI('user/logout/', 'get')
},
getInfo () {
return fetchAPI('user/userinfo/', 'get')
}
}

View File

View File

@ -0,0 +1,69 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="1361px" height="609px" viewBox="0 0 1361 609" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 46.2 (44496) - http://www.bohemiancoding.com/sketch -->
<title>Group 21</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Ant-Design-Pro-3.0" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="账户密码登录-校验" transform="translate(-79.000000, -82.000000)">
<g id="Group-21" transform="translate(77.000000, 73.000000)">
<g id="Group-18" opacity="0.8" transform="translate(74.901416, 569.699158) rotate(-7.000000) translate(-74.901416, -569.699158) translate(4.901416, 525.199158)">
<ellipse id="Oval-11" fill="#CFDAE6" opacity="0.25" cx="63.5748792" cy="32.468367" rx="21.7830479" ry="21.766008"></ellipse>
<ellipse id="Oval-3" fill="#CFDAE6" opacity="0.599999964" cx="5.98746479" cy="13.8668601" rx="5.2173913" ry="5.21330997"></ellipse>
<path d="M38.1354514,88.3520215 C43.8984227,88.3520215 48.570234,83.6838647 48.570234,77.9254015 C48.570234,72.1669383 43.8984227,67.4987816 38.1354514,67.4987816 C32.3724801,67.4987816 27.7006688,72.1669383 27.7006688,77.9254015 C27.7006688,83.6838647 32.3724801,88.3520215 38.1354514,88.3520215 Z" id="Oval-3-Copy" fill="#CFDAE6" opacity="0.45"></path>
<path d="M64.2775582,33.1704963 L119.185836,16.5654915" id="Path-12" stroke="#CFDAE6" stroke-width="1.73913043" stroke-linecap="round" stroke-linejoin="round"></path>
<path d="M42.1431708,26.5002681 L7.71190162,14.5640702" id="Path-16" stroke="#E0B4B7" stroke-width="0.702678964" opacity="0.7" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="1.405357899873153,2.108036953469981"></path>
<path d="M63.9262187,33.521561 L43.6721326,69.3250951" id="Path-15" stroke="#BACAD9" stroke-width="0.702678964" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="1.405357899873153,2.108036953469981"></path>
<g id="Group-17" transform="translate(126.850922, 13.543654) rotate(30.000000) translate(-126.850922, -13.543654) translate(117.285705, 4.381889)" fill="#CFDAE6">
<ellipse id="Oval-4" opacity="0.45" cx="9.13482653" cy="9.12768076" rx="9.13482653" ry="9.12768076"></ellipse>
<path d="M18.2696531,18.2553615 C18.2696531,13.2142826 14.1798519,9.12768076 9.13482653,9.12768076 C4.08980114,9.12768076 0,13.2142826 0,18.2553615 L18.2696531,18.2553615 Z" id="Oval-4" transform="translate(9.134827, 13.691521) scale(-1, -1) translate(-9.134827, -13.691521) "></path>
</g>
</g>
<g id="Group-14" transform="translate(216.294700, 123.725600) rotate(-5.000000) translate(-216.294700, -123.725600) translate(106.294700, 35.225600)">
<ellipse id="Oval-2" fill="#CFDAE6" opacity="0.25" cx="29.1176471" cy="29.1402439" rx="29.1176471" ry="29.1402439"></ellipse>
<ellipse id="Oval-2" fill="#CFDAE6" opacity="0.3" cx="29.1176471" cy="29.1402439" rx="21.5686275" ry="21.5853659"></ellipse>
<ellipse id="Oval-2-Copy" stroke="#CFDAE6" opacity="0.4" cx="179.019608" cy="138.146341" rx="23.7254902" ry="23.7439024"></ellipse>
<ellipse id="Oval-2" fill="#BACAD9" opacity="0.5" cx="29.1176471" cy="29.1402439" rx="10.7843137" ry="10.7926829"></ellipse>
<path d="M29.1176471,39.9329268 L29.1176471,18.347561 C23.1616351,18.347561 18.3333333,23.1796097 18.3333333,29.1402439 C18.3333333,35.1008781 23.1616351,39.9329268 29.1176471,39.9329268 Z" id="Oval-2" fill="#BACAD9"></path>
<g id="Group-9" opacity="0.45" transform="translate(172.000000, 131.000000)" fill="#E6A1A6">
<ellipse id="Oval-2-Copy-2" cx="7.01960784" cy="7.14634146" rx="6.47058824" ry="6.47560976"></ellipse>
<path d="M0.549019608,13.6219512 C4.12262681,13.6219512 7.01960784,10.722722 7.01960784,7.14634146 C7.01960784,3.56996095 4.12262681,0.670731707 0.549019608,0.670731707 L0.549019608,13.6219512 Z" id="Oval-2-Copy-2" transform="translate(3.784314, 7.146341) scale(-1, 1) translate(-3.784314, -7.146341) "></path>
</g>
<ellipse id="Oval-10" fill="#CFDAE6" cx="218.382353" cy="138.685976" rx="1.61764706" ry="1.61890244"></ellipse>
<ellipse id="Oval-10-Copy-2" fill="#E0B4B7" opacity="0.35" cx="179.558824" cy="175.381098" rx="1.61764706" ry="1.61890244"></ellipse>
<ellipse id="Oval-10-Copy" fill="#E0B4B7" opacity="0.35" cx="180.098039" cy="102.530488" rx="2.15686275" ry="2.15853659"></ellipse>
<path d="M28.9985381,29.9671598 L171.151018,132.876024" id="Path-11" stroke="#CFDAE6" opacity="0.8"></path>
</g>
<g id="Group-10" opacity="0.799999952" transform="translate(1054.100635, 36.659317) rotate(-11.000000) translate(-1054.100635, -36.659317) translate(1026.600635, 4.659317)">
<ellipse id="Oval-7" stroke="#CFDAE6" stroke-width="0.941176471" cx="43.8135593" cy="32" rx="11.1864407" ry="11.2941176"></ellipse>
<g id="Group-12" transform="translate(34.596774, 23.111111)" fill="#BACAD9">
<ellipse id="Oval-7" opacity="0.45" cx="9.18534718" cy="8.88888889" rx="8.47457627" ry="8.55614973"></ellipse>
<path d="M9.18534718,17.4450386 C13.8657264,17.4450386 17.6599235,13.6143199 17.6599235,8.88888889 C17.6599235,4.16345787 13.8657264,0.332739156 9.18534718,0.332739156 L9.18534718,17.4450386 Z" id="Oval-7"></path>
</g>
<path d="M34.6597385,24.809694 L5.71666084,4.76878945" id="Path-2" stroke="#CFDAE6" stroke-width="0.941176471"></path>
<ellipse id="Oval" stroke="#CFDAE6" stroke-width="0.941176471" cx="3.26271186" cy="3.29411765" rx="3.26271186" ry="3.29411765"></ellipse>
<ellipse id="Oval-Copy" fill="#F7E1AD" cx="2.79661017" cy="61.1764706" rx="2.79661017" ry="2.82352941"></ellipse>
<path d="M34.6312443,39.2922712 L5.06366663,59.785082" id="Path-10" stroke="#CFDAE6" stroke-width="0.941176471"></path>
</g>
<g id="Group-19" opacity="0.33" transform="translate(1282.537219, 446.502867) rotate(-10.000000) translate(-1282.537219, -446.502867) translate(1142.537219, 327.502867)">
<g id="Group-17" transform="translate(141.333539, 104.502742) rotate(275.000000) translate(-141.333539, -104.502742) translate(129.333539, 92.502742)" fill="#BACAD9">
<circle id="Oval-4" opacity="0.45" cx="11.6666667" cy="11.6666667" r="11.6666667"></circle>
<path d="M23.3333333,23.3333333 C23.3333333,16.8900113 18.1099887,11.6666667 11.6666667,11.6666667 C5.22334459,11.6666667 0,16.8900113 0,23.3333333 L23.3333333,23.3333333 Z" id="Oval-4" transform="translate(11.666667, 17.500000) scale(-1, -1) translate(-11.666667, -17.500000) "></path>
</g>
<circle id="Oval-5-Copy-6" fill="#CFDAE6" cx="201.833333" cy="87.5" r="5.83333333"></circle>
<path d="M143.5,88.8126685 L155.070501,17.6038544" id="Path-17" stroke="#BACAD9" stroke-width="1.16666667"></path>
<path d="M17.5,37.3333333 L127.466252,97.6449735" id="Path-18" stroke="#BACAD9" stroke-width="1.16666667"></path>
<polyline id="Path-19" stroke="#CFDAE6" stroke-width="1.16666667" points="143.902597 120.302281 174.935455 231.571342 38.5 147.510847 126.366941 110.833333"></polyline>
<path d="M159.833333,99.7453842 L195.416667,89.25" id="Path-20" stroke="#E0B4B7" stroke-width="1.16666667" opacity="0.6"></path>
<path d="M205.333333,82.1372105 L238.719406,36.1666667" id="Path-24" stroke="#BACAD9" stroke-width="1.16666667"></path>
<path d="M266.723424,132.231988 L207.083333,90.4166667" id="Path-25" stroke="#CFDAE6" stroke-width="1.16666667"></path>
<circle id="Oval-5" fill="#C1D1E0" cx="156.916667" cy="8.75" r="8.75"></circle>
<circle id="Oval-5-Copy-3" fill="#C1D1E0" cx="39.0833333" cy="148.75" r="5.25"></circle>
<circle id="Oval-5-Copy-2" fill-opacity="0.6" fill="#D1DEED" cx="8.75" cy="33.25" r="8.75"></circle>
<circle id="Oval-5-Copy-4" fill-opacity="0.6" fill="#D1DEED" cx="243.833333" cy="30.3333333" r="5.83333333"></circle>
<circle id="Oval-5-Copy-5" fill="#E0B4B7" cx="175.583333" cy="232.75" r="5.25"></circle>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View File

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="200px" height="200px" viewBox="0 0 200 200" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 47.1 (45422) - http://www.bohemiancoding.com/sketch -->
<title>Group 28 Copy 5</title>
<desc>Created with Sketch.</desc>
<defs>
<linearGradient x1="62.1023273%" y1="0%" x2="108.19718%" y2="37.8635764%" id="linearGradient-1">
<stop stop-color="#4285EB" offset="0%"></stop>
<stop stop-color="#2EC7FF" offset="100%"></stop>
</linearGradient>
<linearGradient x1="69.644116%" y1="0%" x2="54.0428975%" y2="108.456714%" id="linearGradient-2">
<stop stop-color="#29CDFF" offset="0%"></stop>
<stop stop-color="#148EFF" offset="37.8600687%"></stop>
<stop stop-color="#0A60FF" offset="100%"></stop>
</linearGradient>
<linearGradient x1="69.6908165%" y1="-12.9743587%" x2="16.7228981%" y2="117.391248%" id="linearGradient-3">
<stop stop-color="#FA816E" offset="0%"></stop>
<stop stop-color="#F74A5C" offset="41.472606%"></stop>
<stop stop-color="#F51D2C" offset="100%"></stop>
</linearGradient>
<linearGradient x1="68.1279872%" y1="-35.6905737%" x2="30.4400914%" y2="114.942679%" id="linearGradient-4">
<stop stop-color="#FA8E7D" offset="0%"></stop>
<stop stop-color="#F74A5C" offset="51.2635191%"></stop>
<stop stop-color="#F51D2C" offset="100%"></stop>
</linearGradient>
</defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="logo" transform="translate(-20.000000, -20.000000)">
<g id="Group-28-Copy-5" transform="translate(20.000000, 20.000000)">
<g id="Group-27-Copy-3">
<g id="Group-25" fill-rule="nonzero">
<g id="2">
<path d="M91.5880863,4.17652823 L4.17996544,91.5127728 C-0.519240605,96.2081146 -0.519240605,103.791885 4.17996544,108.487227 L91.5880863,195.823472 C96.2872923,200.518814 103.877304,200.518814 108.57651,195.823472 L145.225487,159.204632 C149.433969,154.999611 149.433969,148.181924 145.225487,143.976903 C141.017005,139.771881 134.193707,139.771881 129.985225,143.976903 L102.20193,171.737352 C101.032305,172.906015 99.2571609,172.906015 98.0875359,171.737352 L28.285908,101.993122 C27.1162831,100.824459 27.1162831,99.050775 28.285908,97.8821118 L98.0875359,28.1378823 C99.2571609,26.9692191 101.032305,26.9692191 102.20193,28.1378823 L129.985225,55.8983314 C134.193707,60.1033528 141.017005,60.1033528 145.225487,55.8983314 C149.433969,51.69331 149.433969,44.8756232 145.225487,40.6706018 L108.58055,4.05574592 C103.862049,-0.537986846 96.2692618,-0.500797906 91.5880863,4.17652823 Z" id="Shape" fill="url(#linearGradient-1)"></path>
<path d="M91.5880863,4.17652823 L4.17996544,91.5127728 C-0.519240605,96.2081146 -0.519240605,103.791885 4.17996544,108.487227 L91.5880863,195.823472 C96.2872923,200.518814 103.877304,200.518814 108.57651,195.823472 L145.225487,159.204632 C149.433969,154.999611 149.433969,148.181924 145.225487,143.976903 C141.017005,139.771881 134.193707,139.771881 129.985225,143.976903 L102.20193,171.737352 C101.032305,172.906015 99.2571609,172.906015 98.0875359,171.737352 L28.285908,101.993122 C27.1162831,100.824459 27.1162831,99.050775 28.285908,97.8821118 L98.0875359,28.1378823 C100.999864,25.6271836 105.751642,20.541824 112.729652,19.3524487 C117.915585,18.4685261 123.585219,20.4140239 129.738554,25.1889424 C125.624663,21.0784292 118.571995,14.0340304 108.58055,4.05574592 C103.862049,-0.537986846 96.2692618,-0.500797906 91.5880863,4.17652823 Z" id="Shape" fill="url(#linearGradient-2)"></path>
</g>
<path d="M153.685633,135.854579 C157.894115,140.0596 164.717412,140.0596 168.925894,135.854579 L195.959977,108.842726 C200.659183,104.147384 200.659183,96.5636133 195.960527,91.8688194 L168.690777,64.7181159 C164.472332,60.5180858 157.646868,60.5241425 153.435895,64.7316526 C149.227413,68.936674 149.227413,75.7543607 153.435895,79.9593821 L171.854035,98.3623765 C173.02366,99.5310396 173.02366,101.304724 171.854035,102.473387 L153.685633,120.626849 C149.47715,124.83187 149.47715,131.649557 153.685633,135.854579 Z" id="Shape" fill="url(#linearGradient-3)"></path>
</g>
<ellipse id="Combined-Shape" fill="url(#linearGradient-4)" cx="100.519339" cy="100.436681" rx="23.6001926" ry="23.580786"></ellipse>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.6 KiB

View File

@ -0,0 +1,46 @@
<template>
<tooltip v-if="tips !== ''">
<template slot="title">{{ tips }}</template>
<avatar :size="avatarSize" :src="src" />
</tooltip>
<avatar v-else :size="avatarSize" :src="src" />
</template>
<script>
import Avatar from 'ant-design-vue/es/avatar'
import Tooltip from 'ant-design-vue/es/tooltip'
export default {
name: 'AvatarItem',
components: {
Avatar,
Tooltip
},
props: {
tips: {
type: String,
default: '',
required: false
},
src: {
type: String,
default: ''
}
},
data () {
return {
size: this.$parent.size
}
},
computed: {
avatarSize () {
return this.size !== 'mini' && this.size || 20
}
},
watch: {
'$parent.size' (val) {
this.size = val
}
}
}
</script>

View File

@ -0,0 +1,99 @@
<!--
<template>
<div :class="[prefixCls]">
<ul>
<slot></slot>
<template v-for="item in filterEmpty($slots.default).slice(0, 3)"></template>
<template v-if="maxLength > 0 && filterEmpty($slots.default).length > maxLength">
<avatar-item :size="size">
<avatar :size="size !== 'mini' && size || 20" :style="excessItemsStyle">{{ `+${maxLength}` }}</avatar>
</avatar-item>
</template>
</ul>
</div>
</template>
-->
<script>
import Avatar from 'ant-design-vue/es/avatar'
import AvatarItem from './Item'
import { filterEmpty } from '@/components/_util/util'
export default {
AvatarItem,
name: 'AvatarList',
components: {
Avatar,
AvatarItem
},
props: {
prefixCls: {
type: String,
default: 'ant-pro-avatar-list'
},
/**
* 头像大小 类型: largesmall mini, default
* 默认值: default
*/
size: {
type: [String, Number],
default: 'default'
},
/**
* 要显示的最大项目
*/
maxLength: {
type: Number,
default: 0
},
/**
* 多余的项目风格
*/
excessItemsStyle: {
type: Object,
default: () => {
return {
color: '#f56a00',
backgroundColor: '#fde3cf'
}
}
}
},
data () {
return {}
},
methods: {
getItems (items) {
const classString = {
[`${this.prefixCls}-item`]: true,
[`${this.size}`]: true
}
if (this.maxLength > 0) {
items = items.slice(0, this.maxLength)
items.push((<Avatar size={ this.size } style={ this.excessItemsStyle }>{`+${this.maxLength}`}</Avatar>))
}
const itemList = items.map((item) => (
<li class={ classString }>{ item }</li>
))
return itemList
}
},
render () {
const { prefixCls, size } = this.$props
const classString = {
[`${prefixCls}`]: true,
[`${size}`]: true
}
const items = filterEmpty(this.$slots.default)
const itemsDom = items && items.length ? <ul class={`${prefixCls}-items`}>{ this.getItems(items) }</ul> : null
return (
<div class={ classString }>
{ itemsDom }
</div>
)
}
}
</script>

View File

@ -0,0 +1,4 @@
import AvatarList from './List'
import './index.less'
export default AvatarList

View File

@ -0,0 +1,60 @@
@import "../index";
@avatar-list-prefix-cls: ~"@{ant-pro-prefix}-avatar-list";
@avatar-list-item-prefix-cls: ~"@{ant-pro-prefix}-avatar-list-item";
.@{avatar-list-prefix-cls} {
display: inline-block;
ul {
list-style: none;
display: inline-block;
padding: 0;
margin: 0 0 0 8px;
font-size: 0;
}
}
.@{avatar-list-item-prefix-cls} {
display: inline-block;
font-size: @font-size-base;
margin-left: -8px;
width: @avatar-size-base;
height: @avatar-size-base;
:global {
.ant-avatar {
border: 1px solid #fff;
cursor: pointer;
}
}
&.large {
width: @avatar-size-lg;
height: @avatar-size-lg;
}
&.small {
width: @avatar-size-sm;
height: @avatar-size-sm;
}
&.mini {
width: 20px;
height: 20px;
:global {
.ant-avatar {
width: 20px;
height: 20px;
line-height: 20px;
.ant-avatar-string {
font-size: 12px;
line-height: 18px;
}
}
}
}
}

View File

@ -0,0 +1,64 @@
# AvatarList 用户头像列表
一组用户头像,常用在项目/团队成员列表。可通过设置 `size` 属性来指定头像大小。
引用方式:
```javascript
import AvatarList from '@/components/AvatarList'
const AvatarListItem = AvatarList.AvatarItem
export default {
components: {
AvatarList,
AvatarListItem
}
}
```
## 代码演示 [demo](https://pro.loacg.com/test/home)
```html
<avatar-list size="mini">
<avatar-list-item tips="Jake" src="https://gw.alipayobjects.com/zos/rmsportal/zOsKZmFRdUtvpqCImOVY.png" />
<avatar-list-item tips="Andy" src="https://gw.alipayobjects.com/zos/rmsportal/sfjbOqnsXXJgNCjCzDBL.png" />
<avatar-list-item tips="Niko" src="https://gw.alipayobjects.com/zos/rmsportal/kZzEzemZyKLKFsojXItE.png" />
</avatar-list>
```
```html
<avatar-list :max-length="3">
<avatar-list-item tips="Jake" src="https://gw.alipayobjects.com/zos/rmsportal/zOsKZmFRdUtvpqCImOVY.png" />
<avatar-list-item tips="Andy" src="https://gw.alipayobjects.com/zos/rmsportal/sfjbOqnsXXJgNCjCzDBL.png" />
<avatar-list-item tips="Niko" src="https://gw.alipayobjects.com/zos/rmsportal/kZzEzemZyKLKFsojXItE.png" />
<avatar-list-item tips="Niko" src="https://gw.alipayobjects.com/zos/rmsportal/kZzEzemZyKLKFsojXItE.png" />
<avatar-list-item tips="Niko" src="https://gw.alipayobjects.com/zos/rmsportal/kZzEzemZyKLKFsojXItE.png" />
<avatar-list-item tips="Niko" src="https://gw.alipayobjects.com/zos/rmsportal/kZzEzemZyKLKFsojXItE.png" />
<avatar-list-item tips="Niko" src="https://gw.alipayobjects.com/zos/rmsportal/kZzEzemZyKLKFsojXItE.png" />
</avatar-list>
```
## API
### AvatarList
| 参数 | 说明 | 类型 | 默认值 |
| ---------------- | -------- | ---------------------------------- | --------- |
| size | 头像大小 | `large`、`small` 、`mini`, `default` | `default` |
| maxLength | 要显示的最大项目 | number | - |
| excessItemsStyle | 多余的项目风格 | CSSProperties | - |
### AvatarList.Item
| 参数 | 说明 | 类型 | 默认值 |
| ---- | ------ | --------- | --- |
| tips | 头像展示文案 | string | - |
| src | 头像图片连接 | string | - |

View File

@ -0,0 +1,102 @@
<template>
<span>
{{ lastTime | format }}
</span>
</template>
<script>
function fixedZero (val) {
return val * 1 < 10 ? `0${val}` : val
}
export default {
name: 'CountDown',
props: {
format: {
type: Function,
default: undefined
},
target: {
type: [Date, Number],
required: true
},
onEnd: {
type: Function,
default: () => ({})
}
},
data () {
return {
dateTime: '0',
originTargetTime: 0,
lastTime: 0,
timer: 0,
interval: 1000
}
},
filters: {
format (time) {
const hours = 60 * 60 * 1000
const minutes = 60 * 1000
const h = Math.floor(time / hours)
const m = Math.floor((time - h * hours) / minutes)
const s = Math.floor((time - h * hours - m * minutes) / 1000)
return `${fixedZero(h)}:${fixedZero(m)}:${fixedZero(s)}`
}
},
created () {
this.initTime()
this.tick()
},
methods: {
initTime () {
let lastTime = 0
let targetTime = 0
this.originTargetTime = this.target
try {
if (Object.prototype.toString.call(this.target) === '[object Date]') {
targetTime = this.target
} else {
targetTime = new Date(this.target).getTime()
}
} catch (e) {
throw new Error('invalid target prop')
}
lastTime = targetTime - new Date().getTime()
this.lastTime = lastTime < 0 ? 0 : lastTime
},
tick () {
const { onEnd } = this
this.timer = setTimeout(() => {
if (this.lastTime < this.interval) {
clearTimeout(this.timer)
this.lastTime = 0
if (typeof onEnd === 'function') {
onEnd()
}
} else {
this.lastTime -= this.interval
this.tick()
}
}, this.interval)
}
},
beforeUpdate () {
if (this.originTargetTime !== this.target) {
this.initTime()
}
},
beforeDestroy () {
clearTimeout(this.timer)
}
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,3 @@
import CountDown from './CountDown'
export default CountDown

View File

@ -0,0 +1,34 @@
# CountDown 倒计时
倒计时组件。
引用方式:
```javascript
import CountDown from '@/components/CountDown/CountDown'
export default {
components: {
CountDown
}
}
```
## 代码演示 [demo](https://pro.loacg.com/test/home)
```html
<count-down :target="new Date().getTime() + 3000000" :on-end="onEndHandle" />
```
## API
| 参数 | 说明 | 类型 | 默认值 |
|----------|------------------------------------------|-------------|-------|
| target | 目标时间 | Date | - |
| onEnd | 倒计时结束回调 | funtion | -|

View File

@ -0,0 +1,64 @@
<script>
import Tooltip from 'ant-design-vue/es/tooltip'
import { cutStrByFullLength, getStrFullLength } from '@/components/_util/StringUtil'
/*
const isSupportLineClamp = document.body.style.webkitLineClamp !== undefined;
const TooltipOverlayStyle = {
overflowWrap: 'break-word',
wordWrap: 'break-word',
};
*/
export default {
name: 'Ellipsis',
components: {
Tooltip
},
props: {
prefixCls: {
type: String,
default: 'ant-pro-ellipsis'
},
tooltip: {
type: Boolean
},
length: {
type: Number,
required: true
},
lines: {
type: Number,
default: 1
},
fullWidthRecognition: {
type: Boolean,
default: false
}
},
methods: {
getStrDom (str, fullLength) {
return (
<span>{ cutStrByFullLength(str, this.length) + (fullLength > this.length ? '...' : '') }</span>
)
},
getTooltip (fullStr, fullLength) {
return (
<Tooltip>
<template slot="title">{ fullStr }</template>
{ this.getStrDom(fullStr, fullLength) }
</Tooltip>
)
}
},
render () {
const { tooltip, length } = this.$props
const str = this.$slots.default.map(vNode => vNode.text).join('')
const fullLength = getStrFullLength(str)
const strDom = tooltip && fullLength > length ? this.getTooltip(str, fullLength) : this.getStrDom(str, fullLength)
return (
strDom
)
}
}
</script>

View File

@ -0,0 +1,3 @@
import Ellipsis from './Ellipsis'
export default Ellipsis

View File

@ -0,0 +1,38 @@
# Ellipsis 文本自动省略号
文本过长自动处理省略号,支持按照文本长度和最大行数两种方式截取。
引用方式:
```javascript
import Ellipsis from '@/components/Ellipsis'
export default {
components: {
Ellipsis
}
}
```
## 代码演示 [demo](https://pro.loacg.com/test/home)
```html
<ellipsis :length="100" tooltip>
There were injuries alleged in three cases in 2015, and a
fourth incident in September, according to the safety recall report. After meeting with US regulators in October, the firm decided to issue a voluntary recall.
</ellipsis>
```
## API
参数 | 说明 | 类型 | 默认值
----|------|-----|------
tooltip | 移动到文本展示完整内容的提示 | boolean | -
length | 在按照长度截取下的文本最大字符数,超过则截取省略 | number | -

View File

@ -0,0 +1,30 @@
<template>
<div :class="prefixCls">
<div style="float: left">
<slot name="extra">{{ extra }}</slot>
</div>
<div style="float: right">
<slot></slot>
</div>
</div>
</template>
<script>
export default {
name: 'FooterToolBar',
props: {
prefixCls: {
type: String,
default: 'ant-pro-footer-toolbar'
},
extra: {
type: [String, Object],
default: ''
}
}
}
</script>
<style lang="less" scoped>
</style>

View File

@ -0,0 +1,4 @@
import FooterToolBar from './FooterToolBar'
import './index.less'
export default FooterToolBar

View File

@ -0,0 +1,23 @@
@import "../index";
@footer-toolbar-prefix-cls: ~"@{ant-pro-prefix}-footer-toolbar";
.@{footer-toolbar-prefix-cls} {
position: fixed;
width: 100%;
bottom: 0;
right: 0;
height: 56px;
line-height: 56px;
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.03);
background: #fff;
border-top: 1px solid #e8e8e8;
padding: 0 24px;
z-index: 9;
&:after {
content: "";
display: block;
clear: both;
}
}

View File

@ -0,0 +1,48 @@
# FooterToolbar 底部工具栏
固定在底部的工具栏。
## 何时使用
固定在内容区域的底部,不随滚动条移动,常用于长页面的数据搜集和提交工作。
引用方式:
```javascript
import FooterToolBar from '@/components/FooterToolbar'
export default {
components: {
FooterToolBar
}
}
```
## 代码演示
```html
<footer-tool-bar>
<a-button type="primary" @click="validate" :loading="loading">提交</a-button>
</footer-tool-bar>
```
```html
<footer-tool-bar extra="扩展信息提示">
<a-button type="primary" @click="validate" :loading="loading">提交</a-button>
</footer-tool-bar>
```
## API
参数 | 说明 | 类型 | 默认值
----|------|-----|------
children (slot) | 工具栏内容,向右对齐 | - | -
extra | 额外信息,向左对齐 | String, Object | -

View File

@ -0,0 +1,56 @@
<template>
<div style="margin: -23px -24px 24px -24px">
<a-tabs
hideAdd
v-model="activeKey"
type="editable-card"
:tabBarStyle="{ background: '#FFF', margin: 0, paddingLeft: '16px', paddingTop: '1px' }"
@edit="onEdit"
>
<a-tab-pane v-for="page in pages" :style="{ height: 0 }" :tab="page.meta.title" :key="page.fullPath" :closable="pages.length > 1">
</a-tab-pane>
</a-tabs>
</div>
</template>
<script>
export default {
name: 'MultiTab',
data () {
return {
fullPathList: [],
pages: [],
activeKey: '',
newTabIndex: 0
}
},
created () {
this.pages.push(this.$route)
this.fullPathList.push(this.$route.fullPath)
},
methods: {
onEdit (targetKey, action) {
this[action](targetKey)
},
remove (targetKey) {
if (this.pages.length === 1) {
return
}
this.pages = this.pages.filter(page => page.fullPath !== targetKey)
this.fullPathList = this.fullPathList.filter(path => path !== targetKey)
}
},
watch: {
'$route': function (newVal) {
this.activeKey = newVal.fullPath
if (this.fullPathList.indexOf(newVal.fullPath) < 0) {
this.fullPathList.push(newVal.fullPath)
this.pages.push(newVal)
}
},
activeKey: function (newPathKey) {
this.$router.push({ path: newPathKey })
}
}
}
</script>

View File

@ -0,0 +1,2 @@
import MultiTab from './MultiTab'
export default MultiTab

View File

@ -0,0 +1,54 @@
<template>
<div :class="[prefixCls]">
<slot name="subtitle">
<div :class="[`${prefixCls}-subtitle`]">{{ typeof subTitle === 'string' ? subTitle : subTitle() }}</div>
</slot>
<div class="number-info-value">
<span>{{ total }}</span>
<span class="sub-total">
{{ subTotal }}
<icon :type="`caret-${status}`" />
</span>
</div>
</div>
</template>
<script>
import Icon from 'ant-design-vue/es/icon'
export default {
name: 'NumberInfo',
props: {
prefixCls: {
type: String,
default: 'ant-pro-number-info'
},
total: {
type: Number,
required: true
},
subTotal: {
type: Number,
required: true
},
subTitle: {
type: [String, Function],
default: ''
},
status: {
type: String,
default: 'up'
}
},
components: {
Icon
},
data () {
return {}
}
}
</script>
<style lang="less" scoped>
@import "index";
</style>

View File

@ -0,0 +1,3 @@
import NumberInfo from './NumberInfo'
export default NumberInfo

View File

@ -0,0 +1,55 @@
@import "../index";
@numberInfo-prefix-cls: ~"@{ant-pro-prefix}-number-info";
.@{numberInfo-prefix-cls} {
.ant-pro-number-info-subtitle {
color: @text-color-secondary;
font-size: @font-size-base;
height: 22px;
line-height: 22px;
overflow: hidden;
text-overflow: ellipsis;
word-break: break-all;
white-space: nowrap;
}
.number-info-value {
margin-top: 4px;
font-size: 0;
overflow: hidden;
text-overflow: ellipsis;
word-break: break-all;
white-space: nowrap;
& > span {
color: @heading-color;
display: inline-block;
line-height: 32px;
height: 32px;
font-size: 24px;
margin-right: 32px;
}
.sub-total {
color: @text-color-secondary;
font-size: @font-size-lg;
vertical-align: top;
margin-right: 0;
i {
font-size: 12px;
transform: scale(0.82);
margin-left: 4px;
}
:global {
.anticon-caret-up {
color: @red-6;
}
.anticon-caret-down {
color: @green-6;
}
}
}
}
}

View File

@ -0,0 +1,43 @@
# NumberInfo 数据文本
常用在数据卡片中,用于突出展示某个业务数据。
引用方式:
```javascript
import NumberInfo from '@/components/NumberInfo'
export default {
components: {
NumberInfo
}
}
```
## 代码演示 [demo](https://pro.loacg.com/test/home)
```html
<number-info
:sub-title="() => { return 'Visits this week' }"
:total="12321"
status="up"
:sub-total="17.1"></number-info>
```
## API
参数 | 说明 | 类型 | 默认值
----|------|-----|------
title | 标题 | ReactNode\|string | -
subTitle | 子标题 | ReactNode\|string | -
total | 总量 | ReactNode\|string | -
subTotal | 子总量 | ReactNode\|string | -
status | 增加状态 | 'up \| down' | -
theme | 状态样式 | string | 'light'
gap | 设置数字和描述之间的间距(像素)| number | 8

View File

@ -0,0 +1,124 @@
import { Menu, Icon, Input } from 'ant-design-vue'
const { Item, ItemGroup, SubMenu } = Menu
const { Search } = Input
export default {
name: 'Tree',
props: {
dataSource: {
type: Array,
required: true
},
openKeys: {
type: Array,
default: () => []
},
search: {
type: Boolean,
default: false
}
},
created () {
this.localOpenKeys = this.openKeys.slice(0)
},
data () {
return {
localOpenKeys: []
}
},
methods: {
handlePlus (item) {
this.$emit('add', item)
},
handleTitleClick (...args) {
this.$emit('titleClick', { args })
},
renderSearch () {
return (
<Search
placeholder="input search text"
style="width: 100%; margin-bottom: 1rem"
/>
)
},
renderIcon (icon) {
return icon && (<Icon type={icon} />) || null
},
renderMenuItem (item) {
return (
<Item key={item.key}>
{ this.renderIcon(item.icon) }
{ item.title }
<a class="btn" style="width: 20px;z-index:1300" {...{ on: { click: () => this.handlePlus(item) } }}><a-icon type="plus"/></a>
</Item>
)
},
renderItem (item) {
return item.children ? this.renderSubItem(item, item.key) : this.renderMenuItem(item, item.key)
},
renderItemGroup (item) {
const childrenItems = item.children.map(o => {
return this.renderItem(o, o.key)
})
return (
<ItemGroup key={item.key}>
<template slot="title">
<span>{ item.title }</span>
<a-dropdown>
<a class="btn"><a-icon type="ellipsis" /></a>
<a-menu slot="overlay">
<a-menu-item key="1">新增</a-menu-item>
<a-menu-item key="2">合并</a-menu-item>
<a-menu-item key="3">移除</a-menu-item>
</a-menu>
</a-dropdown>
</template>
{ childrenItems }
</ItemGroup>
)
},
renderSubItem (item, key) {
const childrenItems = item.children && item.children.map(o => {
return this.renderItem(o, o.key)
})
const title = (
<span slot="title">
{ this.renderIcon(item.icon) }
<span>{ item.title }</span>
</span>
)
if (item.group) {
return this.renderItemGroup(item)
}
// titleClick={this.handleTitleClick(item)}
return (
<SubMenu key={key}>
{ title }
{ childrenItems }
</SubMenu>
)
}
},
render () {
const { dataSource, search } = this.$props
// this.localOpenKeys = openKeys.slice(0)
const list = dataSource.map(item => {
return this.renderItem(item)
})
return (
<div class="tree-wrapper">
{ search ? this.renderSearch() : null }
<Menu mode="inline" class="custom-tree" {...{ on: { click: item => this.$emit('click', item), 'update:openKeys': val => { this.localOpenKeys = val } } }} openKeys={this.localOpenKeys}>
{ list }
</Menu>
</div>
)
}
}

View File

@ -0,0 +1,41 @@
<template>
<div :class="[prefixCls, reverseColor && 'reverse-color' ]">
<span>
<slot name="term"></slot>
<span class="item-text">
<slot></slot>
</span>
</span>
<span :class="[flag]"><a-icon :type="`caret-${flag}`"/></span>
</div>
</template>
<script>
export default {
name: 'Trend',
props: {
prefixCls: {
type: String,
default: 'ant-pro-trend'
},
/**
* 上升下降标识up|down
*/
flag: {
type: String,
required: true
},
/**
* 颜色反转
*/
reverseColor: {
type: Boolean,
default: false
}
}
}
</script>
<style lang="less" scoped>
@import "index";
</style>

View File

@ -0,0 +1,3 @@
import Trend from './Trend.vue'
export default Trend

View File

@ -0,0 +1,42 @@
@import "../index";
@trend-prefix-cls: ~"@{ant-pro-prefix}-trend";
.@{trend-prefix-cls} {
display: inline-block;
font-size: @font-size-base;
line-height: 22px;
.up,
.down {
margin-left: 4px;
position: relative;
top: 1px;
i {
font-size: 12px;
transform: scale(0.83);
}
}
.item-text {
display: inline-block;
margin-left: 8px;
color: rgba(0,0,0,.85);
}
.up {
color: @red-6;
}
.down {
color: @green-6;
top: -1px;
}
&.reverse-color .up {
color: @green-6;
}
&.reverse-color .down {
color: @red-6;
}
}

View File

@ -0,0 +1,45 @@
# Trend 趋势标记
趋势符号,标记上升和下降趋势。通常用绿色代表“好”,红色代表“不好”,股票涨跌场景除外。
引用方式:
```javascript
import Trend from '@/components/Trend'
export default {
components: {
Trend
}
}
```
## 代码演示 [demo](https://pro.loacg.com/test/home)
```html
<trend flag="up">5%</trend>
```
```html
<trend flag="up">
<span slot="term">工资</span>
5%
</trend>
```
```html
<trend flag="up" term="工资">5%</trend>
```
## API
| 参数 | 说明 | 类型 | 默认值 |
|----------|------------------------------------------|-------------|-------|
| flag | 上升下降标识:`up|down` | string | - |
| reverseColor | 颜色反转 | Boolean | false |

View File

@ -0,0 +1,25 @@
export const getStrFullLength = (str = '') =>
str.split('').reduce((pre, cur) => {
const charCode = cur.charCodeAt(0)
if (charCode >= 0 && charCode <= 128) {
return pre + 1
}
return pre + 2
}, 0)
export const cutStrByFullLength = (str = '', maxLength) => {
let showLength = 0
return str.split('').reduce((pre, cur) => {
const charCode = cur.charCodeAt(0)
if (charCode >= 0 && charCode <= 128) {
showLength += 1
} else {
showLength += 2
}
if (showLength <= maxLength) {
return pre + cur
}
return pre
}, '')
}

View File

@ -0,0 +1,12 @@
/**
* components util
*/
/**
* 清理空值对象
* @param children
* @returns {*[]}
*/
export function filterEmpty (children = []) {
return children.filter(c => c.tag || (c.text && c.text.trim() !== ''))
}

View File

@ -0,0 +1,57 @@
<template>
<div :style="{ padding: '0 0 32px 32px' }">
<h4 :style="{ marginBottom: '20px' }">{{ title }}</h4>
<v-chart
height="254"
:data="data"
:forceFit="true"
:padding="['auto', 'auto', '40', '50']">
<v-tooltip />
<v-axis />
<v-bar position="x*y"/>
</v-chart>
</div>
</template>
<script>
const data = []
for (let i = 0; i < 12; i += 1) {
data.push({
x: `${i + 1}`,
y: Math.floor(Math.random() * 1000) + 200
})
}
const tooltip = [
'x*y',
(x, y) => ({
name: x,
value: y
})
]
const scale = [{
dataKey: 'x',
min: 2
}, {
dataKey: 'y',
title: '时间',
min: 1,
max: 22
}]
export default {
name: 'Bar',
props: {
title: {
type: String,
default: ''
}
},
data () {
return {
data,
scale,
tooltip
}
}
}
</script>

View File

@ -0,0 +1,111 @@
<template>
<a-card :loading="loading" :body-style="{ padding: '20px 24px 8px' }" :bordered="false">
<div class="chart-card-header">
<div class="meta">
<span class="chart-card-title">{{ title }}</span>
<span class="chart-card-action">
<slot name="action"></slot>
</span>
</div>
<div class="total"><span>{{ total }}</span></div>
</div>
<div class="chart-card-content">
<div class="content-fix">
<slot></slot>
</div>
</div>
<div class="chart-card-footer">
<div class="field">
<slot name="footer"></slot>
</div>
</div>
</a-card>
</template>
<script>
export default {
name: 'ChartCard',
props: {
title: {
type: String,
default: ''
},
total: {
type: String,
default: ''
},
loading: {
type: Boolean,
default: false
}
}
}
</script>
<style lang="less" scoped>
.chart-card-header {
position: relative;
overflow: hidden;
width: 100%;
.meta {
position: relative;
overflow: hidden;
width: 100%;
color: rgba(0, 0, 0, .45);
font-size: 14px;
line-height: 22px;
}
}
.chart-card-action {
cursor: pointer;
position: absolute;
top: 0;
right: 0;
}
.chart-card-footer {
border-top: 1px solid #e8e8e8;
padding-top: 9px;
margin-top: 8px;
> * {
position: relative;
}
.field {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin: 0;
}
}
.chart-card-content {
margin-bottom: 12px;
position: relative;
height: 46px;
width: 100%;
.content-fix {
position: absolute;
left: 0;
bottom: 0;
width: 100%;
}
}
.total {
overflow: hidden;
text-overflow: ellipsis;
word-break: break-all;
white-space: nowrap;
color: #000;
margin-top: 4px;
margin-bottom: 0;
font-size: 30px;
line-height: 38px;
height: 38px;
}
</style>

View File

@ -0,0 +1,67 @@
<template>
<div>
<v-chart
:forceFit="true"
:height="height"
:width="width"
:data="data"
:scale="scale"
:padding="0">
<v-tooltip />
<v-interval
:shape="['liquid-fill-gauge']"
position="transfer*value"
color=""
:v-style="{
lineWidth: 10,
opacity: 0.75
}"
:tooltip="[
'transfer*value',
(transfer, value) => {
return {
name: transfer,
value,
};
},
]"
></v-interval>
<v-guide
v-for="(row, index) in data"
:key="index"
type="text"
:top="true"
:position="{
gender: row.transfer,
value: 45
}"
:content="row.value + '%'"
:v-style="{
fontSize: 100,
textAlign: 'center',
opacity: 0.75,
}"
/>
</v-chart>
</div>
</template>
<script>
export default {
name: 'Liquid',
props: {
height: {
type: Number,
default: 0
},
width: {
type: Number,
default: 0
}
}
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,56 @@
<template>
<div class="antv-chart-mini">
<div class="chart-wrapper" :style="{ height: 46 }">
<v-chart :force-fit="true" :height="height" :data="data" :padding="[36, 0, 18, 0]">
<v-tooltip />
<v-smooth-area position="x*y" />
</v-chart>
</div>
</div>
</template>
<script>
import moment from 'moment'
const data = []
const beginDay = new Date().getTime()
for (let i = 0; i < 10; i++) {
data.push({
x: moment(new Date(beginDay + 1000 * 60 * 60 * 24 * i)).format('YYYY-MM-DD'),
y: Math.round(Math.random() * 10)
})
}
const tooltip = [
'x*y',
(x, y) => ({
name: x,
value: y
})
]
const scale = [{
dataKey: 'x',
min: 2
}, {
dataKey: 'y',
title: '时间',
min: 1,
max: 22
}]
export default {
name: 'MiniArea',
data () {
return {
data,
tooltip,
scale,
height: 100
}
}
}
</script>
<style lang="less" scoped>
@import "chart";
</style>

View File

@ -0,0 +1,57 @@
<template>
<div class="antv-chart-mini">
<div class="chart-wrapper" :style="{ height: 46 }">
<v-chart :force-fit="true" :height="height" :data="data" :padding="[36, 5, 18, 5]">
<v-tooltip />
<v-bar position="x*y" />
</v-chart>
</div>
</div>
</template>
<script>
import moment from 'moment'
const data = []
const beginDay = new Date().getTime()
for (let i = 0; i < 10; i++) {
data.push({
x: moment(new Date(beginDay + 1000 * 60 * 60 * 24 * i)).format('YYYY-MM-DD'),
y: Math.round(Math.random() * 10)
})
}
const tooltip = [
'x*y',
(x, y) => ({
name: x,
value: y
})
]
const scale = [{
dataKey: 'x',
min: 2
}, {
dataKey: 'y',
title: '时间',
min: 1,
max: 30
}]
export default {
name: 'MiniBar',
data () {
return {
data,
tooltip,
scale,
height: 100
}
}
}
</script>
<style lang="less" scoped>
@import "chart";
</style>

View File

@ -0,0 +1,75 @@
<template>
<div class="chart-mini-progress">
<div class="target" :style="{ left: target + '%'}">
<span :style="{ backgroundColor: color }" />
<span :style="{ backgroundColor: color }"/>
</div>
<div class="progress-wrapper">
<div class="progress" :style="{ backgroundColor: color, width: percentage + '%', height: height }"></div>
</div>
</div>
</template>
<script>
export default {
name: 'MiniProgress',
props: {
target: {
type: Number,
default: 0
},
height: {
type: String,
default: '10px'
},
color: {
type: String,
default: '#13C2C2'
},
percentage: {
type: Number,
default: 0
}
}
}
</script>
<style lang="less" scoped>
.chart-mini-progress {
padding: 5px 0;
position: relative;
width: 100%;
.target {
position: absolute;
top: 0;
bottom: 0;
span {
border-radius: 100px;
position: absolute;
top: 0;
left: 0;
height: 4px;
width: 2px;
&:last-child {
top: auto;
bottom: 0;
}
}
}
.progress-wrapper {
background-color: #f5f5f5;
position: relative;
.progress {
transition: all .4s cubic-bezier(.08,.82,.17,1) 0s;
border-radius: 1px 0 0 1px;
background-color: #1890ff;
width: 0;
height: 100%;
}
}
}
</style>

View File

@ -0,0 +1,68 @@
<template>
<v-chart :forceFit="true" height="400" :data="data" :padding="[20, 20, 95, 20]" :scale="scale">
<v-tooltip></v-tooltip>
<v-axis :dataKey="axis1Opts.dataKey" :line="axis1Opts.line" :tickLine="axis1Opts.tickLine" :grid="axis1Opts.grid" />
<v-axis :dataKey="axis2Opts.dataKey" :line="axis2Opts.line" :tickLine="axis2Opts.tickLine" :grid="axis2Opts.grid" />
<v-legend dataKey="user" marker="circle" :offset="30" />
<v-coord type="polar" radius="0.8" />
<v-line position="item*score" color="user" :size="2" />
<v-point position="item*score" color="user" :size="4" shape="circle" />
</v-chart>
</template>
<script>
const axis1Opts = {
dataKey: 'item',
line: null,
tickLine: null,
grid: {
lineStyle: {
lineDash: null
},
hideFirstLine: false
}
}
const axis2Opts = {
dataKey: 'score',
line: null,
tickLine: null,
grid: {
type: 'polygon',
lineStyle: {
lineDash: null
}
}
}
const scale = [
{
dataKey: 'score',
min: 0,
max: 80
}, {
dataKey: 'user',
alias: '类型'
}
]
export default {
name: 'Radar',
props: {
data: {
type: Array,
default: null
}
},
data () {
return {
axis1Opts,
axis2Opts,
scale
}
}
}
</script>
<style scoped>
</style>

Some files were not shown because too many files have changed in this diff Show More