diff --git a/.eslintrc.js b/.eslintrc.js index 776484b90c8e74d31e2006bb47b54c34f1ec70ef..a4933a5eab37056be30fb2dce815485677350ab3 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -19,6 +19,7 @@ module.exports = { 'quote-props': ['warn', 'as-needed'], 'comma-dangle': ['error', 'only-multiline'], camelcase: ['error', { properties: 'never' }], + 'array-bracket-spacing': 'warn', 'arrow-spacing': 'warn', 'block-spacing': 'warn', @@ -30,12 +31,22 @@ module.exports = { 'object-curly-spacing': ['warn', 'always'], 'rest-spread-spacing': 'warn', 'switch-colon-spacing': 'error', + 'func-call-spacing': 'warn', 'semi-spacing': 'warn', 'template-curly-spacing': 'warn', 'template-tag-spacing': 'warn', 'yield-star-spacing': 'warn', - semi: ['warn', 'always'], + 'space-unary-ops': 'warn', + 'no-multi-spaces': 'warn', + 'no-mixed-spaces-and-tabs': 'warn', 'no-trailing-spaces': 'warn', + 'spaced-comment': 'warn', + 'space-infix-ops': 'warn', + + 'eol-last': 'warn', + indent: ['warn', 2], + + semi: ['warn', 'always'], 'prefer-template': 'error', 'prefer-spread': 'error', 'no-var': 'error', @@ -51,9 +62,13 @@ module.exports = { ignoreTemplateLiterals: true, ignoreStrings: true, ignorePattern: 'd="([\\s\\S]*?)"', + ignoreComments: true, }], 'default-param-last': 'off', 'no-param-reassign': ['error', { props: false }], + 'no-shadow': 'off', + '@typescript-eslint/no-shadow': ['error', { hoist: 'all' }], + 'no-use-before-define': ['error', { functions: false }], 'vue/max-attributes-per-line': 'off', 'vue/html-self-closing': ['warn', { @@ -66,8 +81,16 @@ module.exports = { 'vue/singleline-html-element-content-newline': 'off', 'vue/html-closing-bracket-newline': 'off', 'vue/multiline-html-element-content-newline': 'warn', - + 'vue/no-mutating-props': ['error', { shallowOnly: true }], 'no-unused-vars': 'off', - '@typescript-eslint/no-unused-vars': 'warn', + '@typescript-eslint/no-unused-vars': [ + 'warn', + { + varsIgnorePattern: '^_', + argsIgnorePattern: '^_', + caughtErrorsIgnorePattern: '^_' + } + ], + 'no-undef-init': 'error', }, -}; \ No newline at end of file +}; diff --git a/.vscode/extensions.json b/.vscode/extensions.json index a7cea0b0678120a1b590d1b6592c7318039b9179..b755a5962a952a53e6a04aab26fd4155a15146dc 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,3 +1,6 @@ { - "recommendations": ["Vue.volar"] + "recommendations": [ + "Vue.volar", + "antfu.pnpm-catalog-lens" + ] } diff --git a/.vscode/screen.code-snippets b/.vscode/screen.code-snippets new file mode 100644 index 0000000000000000000000000000000000000000..6d9b273bd1de9e6732361db899818088aa79d797 --- /dev/null +++ b/.vscode/screen.code-snippets @@ -0,0 +1,222 @@ +{ + "(0, 600)": { + "prefix": ["w-600", "@include respond-to('phone')"], + "body": [ + "@include respond-to('phone') {", + " $1", + "}" + ], + "description": "(0, 600px)", + "scope": "scss, less" + }, + "(601, Inf)": { + "prefix": ["w600", "@include respond-to('>phone')"], + "body": [ + "@include respond-to('>phone') {", + " $1", + "}" + ], + "description": "(601px, Inf)", + "scope": "scss, less" + }, + "(601, 1200)": { + "prefix": ["w600-1200", "@include respond-to('pad')"], + "body": [ + "@include respond-to('pad') {", + " $1", + "}" + ], + "description": "(601px, 1200px)", + "scope": "scss, less" + }, + "(0, 1200)": { + "prefix": ["w-1200", "@include respond-to('<=pad')"], + "body": [ + "@include respond-to('<=pad') {", + " $1", + "}" + ], + "description": "(0, 1200px)", + "scope": "scss, less" + }, + "(1201, Inf)": { + "prefix": ["w1200", "@include respond-to('>pad')"], + "body": [ + "@include respond-to('>pad') {", + " $1", + "}" + ], + "description": "(1201px, Inf)", + "scope": "scss, less" + }, + "(601, 840)": { + "prefix": ["w600-840", "@include respond-to('pad_v')"], + "body": [ + "@include respond-to('pad_v') {", + " $1", + "}" + ], + "description": "(601px, 840px)", + "scope": "scss, less" + }, + "(0, 840)": { + "prefix": ["w-840", "@include respond-to('<=pad_v')"], + "body": [ + "@include respond-to('<=pad_v') {", + " $1", + "}" + ], + "description": "(0, 840px)", + "scope": "scss, less" + }, + "(841, Inf)": { + "prefix": ["w840", "@include respond-to('>pad_v')"], + "body": [ + "@include respond-to('>pad_v') {", + " $1", + "}" + ], + "description": "(841px, Inf)", + "scope": "scss, less" + }, + "(841, 1200)": { + "prefix": ["w840-1200", "@include respond-to('pad_h')"], + "body": [ + "@include respond-to('pad_h') {", + " $1", + "}" + ], + "description": "(841px, 1200px)", + "scope": "scss, less" + }, + "(1200, 1440)": { + "prefix": ["w1200-1440", "@include respond-to('laptop')"], + "body": [ + "@include respond-to('laptop') {", + " $1", + "}" + ], + "description": "(1200px, 1440px)", + "scope": "scss, less" + }, + "(0, 1440)": { + "prefix": ["w-1440", "@include respond-to('<=laptop')"], + "body": [ + "@include respond-to('<=laptop') {", + " $1", + "}" + ], + "description": "(0, 1440px)", + "scope": "scss, less" + }, + "(1441, Inf)": { + "prefix": ["w1440", "@include respond-to('>laptop')"], + "body": [ + "@include respond-to('>laptop') {", + " $1", + "}" + ], + "description": "(1441px, Inf)", + "scope": "scss, less" + }, + "(601, 1440)": { + "prefix": ["w600-1440", "@include responsd-to('pad-laptop')"], + "body": [ + "@include respond-to('pad-laptop') {", + " $1", + "}" + ], + "description": "(601px, 1440px)", + "scope": "scss, less" + }, + "(841, 1440)": { + "prefix": ["w840-1440", "@include responsd-to('pad_v-laptop')"], + "body": [ + "@include respond-to('pad_v-laptop') {", + " $1", + "}" + ], + "description": "(841px, 1440px)", + "scope": "scss, less" + }, + "(840, 1680)": { + "prefix": ["w840-1680", "@include responsd-to('pad_v-pc_s')"], + "body": [ + "@include respond-to('pad_v-pc_s') {", + " $1 ", + "}" + ], + "description": "(840px, 1680px)", + "scope": "scss, less" + }, + "(1441-1920)": { + "prefix": ["w1440-1920", "@include responsd-to('pc')"], + "body": [ + "@include respond-to('pc') {", + " $1", + "}" + ], + "description": "(1441-1920px)", + "scope": "scss, less" + }, + "(1441, 1680)": { + "prefix": ["w1440-1680", "@include responsd-to('pc_s')"], + "body": [ + "@include respond-to('pc_s') {", + " $1", + "}" + ], + "description": "(1441, 1680px)", + "scope": "scss, less" + }, + "(0, 1680)": { + "prefix": ["w-1680", "@include responsd-to('<=pc_s')"], + "body": [ + "@include respond-to('<=pc_s') {", + " $1", + "}" + ], + "description": "(0, 1680px)", + "scope": "scss, less" + }, + "(1681, 1920)": { + "prefix": ["w1680-1920", "@include responsd-to('pc_l')"], + "body": [ + "@include respond-to('pc_l') {", + " $1", + "}" + ], + "description": "(1681, 1920px)", + "scope": "scss, less" + }, + "(1681, Inf)": { + "prefix": ["w1680", "@include responsd-to('>pc_s')"], + "body": [ + "@include respond-to('>pc_s') {", + " $1", + "}" + ], + "description": "(1681px, Inf)", + "scope": "scss, less" + }, + "(1921, Inf)": { + "prefix": ["w1920", "@include responsd-to('>pc')"], + "body": [ + "@include respond-to('>pc') {", + " $1", + "}" + ], + "description": "(1921px, Inf)", + "scope": "scss, less" + }, + "(1201, 1680)": { + "prefix": ["w1200-1680", "@include responsd-to('laptop-pc_s')"], + "body": [ + "@include respond-to('laptop-pc_s') {", + " $1", + "}" + ], + "description": "(1201px, 1680px)", + "scope": "scss, less" + } +} diff --git a/.vscode/settings.json b/.vscode/settings.json index b8b8dd50c04e72d3e1a477ef77bcfe8729842f61..89532e0246e2e7bea74ada6738a1d7e688bef0b0 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,12 +3,15 @@ "explorer.fileNesting.patterns": { "package.json": "package-lock.json, yarn.lock, pnpm-lock.yaml,pnpm-workspace.yaml,.npmrc,.gitignore,LICENSE.md", ".eslintrc.js": ".prettierrc.js", - "README.md": "README.en.md", + "README.md": "README.en.md" }, "outline.showFiles": true, "files.exclude": { "packages/opendesign/dist": true, "packages/opendesign/es": true, "packages/opendesign/lib": true + }, + "editor.codeActionsOnSave": { + "source.fixAll": "explicit" } -} \ No newline at end of file +} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..6f864c4c4b44e3cddfdec95a3a5585d3ee7d54fc --- /dev/null +++ b/Dockerfile @@ -0,0 +1,159 @@ +FROM swr.cn-north-4.myhuaweicloud.com/opensourceway/node:latest as Builder + +RUN mkdir -p /home/openDesign/web +WORKDIR /home/openDesign/web +COPY . /home/openDesign/web + +RUN npm install pnpm -g && \ + pnpm docs:install && \ + pnpm docs:build + +FROM swr.cn-north-4.myhuaweicloud.com/opensourceway/openeuler/nginx:latest as NginxBuilder + +FROM swr.cn-north-4.myhuaweicloud.com/opensourceway/openeuler/base:latest + +ENV NGINX_CONFIG_FILE /etc/nginx/nginx.conf +ENV NGINX_CONFIG_PATH /etc/nginx/ +ENV NGINX_PID /var/run/nginx.pid +ENV NGINX_USER nginx +ENV NGINX_GROUP nginx +ENV NGINX_BIN /usr/share/nginx/sbin/ +ENV NGINX_HOME /usr/share/nginx/ +ENV NGINX_EXE_FILE /usr/share/nginx/sbin/nginx +ENV DST_PATH /etc/nginx/cert + +COPY --from=NginxBuilder /usr/share/nginx /usr/share/nginx +COPY --from=NginxBuilder /usr/share/nginx/sbin/nginx /usr/share/nginx/sbin/nginx +COPY --from=NginxBuilder /etc/nginx/modules /etc/nginx/modules +COPY --from=NginxBuilder /etc/nginx/geoip /etc/nginx/geoip +COPY --from=NginxBuilder /etc/nginx/mime.types /etc/nginx/mime.types +COPY --from=Builder /home/openDesign/web/packages/docs/dist /usr/share/nginx/www/ +COPY ./deploy/monitor.sh ./deploy/entrypoint.sh /etc/nginx/ +COPY ./deploy/nginx/nginx.conf /etc/nginx/nginx.conf.template + +RUN sed -i "s|repo.openeuler.org|mirrors.nju.edu.cn/openeuler|g" /etc/yum.repos.d/openEuler.repo \ + && sed -i '/metalink/d' /etc/yum.repos.d/openEuler.repo \ + && sed -i '/metadata_expire/d' /etc/yum.repos.d/openEuler.repo \ + && yum update -y \ + && yum install -y findutils passwd shadow pcre-devel net-tools libmaxminddb libmaxminddb-devel \ + && find /usr/share/nginx/www -type d -print0| xargs -0 chmod 500 \ + && find /usr/share/nginx/www -type f -print0| xargs -0 chmod 400 \ + && touch /var/run/nginx.pid \ + && groupadd -g 1000 nginx \ + && useradd -u 1000 -g nginx -s /sbin/nologin nginx \ + && sed -i '/^PATH="\$HOME\/\.local\/bin:\$HOME\/bin:\$PATH"/d; /^export PATH/d' /home/nginx/.bashrc \ + && chmod 750 /usr \ + && chmod 550 /usr/share \ + && chown -R nginx:nginx /usr/share/nginx \ + && find /usr/share/nginx -type d -print0 | xargs -0 chmod 500 \ + && chmod 500 /usr/share/nginx/sbin/nginx \ + && mkdir -p /var/log/nginx \ + && mkdir -p /etc/nginx/cert \ + && chown -R nginx:nginx /etc/nginx/cert \ + && chmod -R 700 /etc/nginx/cert \ + && chown -R nginx:nginx /var/log/nginx \ + && chmod -R 640 /var/log/nginx \ + && touch /var/log/nginx/error.log \ + && touch /var/log/nginx/access.log \ + && chmod 640 /var/log/nginx/error.log \ + && chmod 640 /var/log/nginx/access.log \ + && chmod 640 /var/log/dnf.librepo.log \ + && chmod 640 /var/log/dnf.log \ + && chmod 640 /var/log/dnf.rpm.log \ + && chmod 640 /var/log/hawkey.log \ + && chmod 640 /var/log/*.log \ + && chmod 440 /etc/nginx/nginx*.conf* \ + && chown -R nginx:nginx /var/log/nginx/* \ + && mkdir -p /var/lib/nginx/tmp/client_body \ + && chown -R nginx:nginx /var/lib/nginx/tmp/client_body \ + && mkdir -p /var/lib/nginx/tmp/fastcgi \ + && chown -R nginx:nginx /var/lib/nginx/tmp/fastcgi \ + && mkdir -p /var/lib/nginx/tmp/proxy \ + && chown -R nginx:nginx /var/lib/nginx/tmp/proxy \ + && mkdir -p /var/lib/nginx/tmp/scgi \ + && chown -R nginx:nginx /var/lib/nginx/tmp/scgi \ + && mkdir -p /var/lib/nginx/tmp/uwsgi \ + && chown -R nginx:nginx /var/lib/nginx/tmp/uwsgi \ + && chmod -R 500 /var/lib/nginx/ \ + && chmod -R 750 /var/lib/nginx/tmp/proxy \ + && chown -R nginx:nginx /var/lib/nginx/ \ + && chown -R nginx:nginx /var/run/nginx.pid \ + && chmod 640 /var/run/nginx.pid \ + && chown -R nginx:nginx /etc/nginx \ + && chmod 550 /etc/nginx \ + && chmod 550 /etc/nginx/geoip/ \ + && chmod 440 /etc/nginx/geoip/* \ + && chmod 550 /etc/nginx/modules \ + && chmod 440 /etc/nginx/modules/* \ + && touch /etc/nginx/nginx.conf \ + && chown nginx:nginx /etc/nginx/nginx.conf \ + && chmod 640 /etc/nginx/nginx.conf \ + && chmod 640 /etc/nginx/nginx.conf.template \ + && chmod 440 /etc/nginx/mime.types \ + && chmod 700 /var/lib/nginx/tmp/client_body \ + && lsd() { \ + local v="$1"; \ + ls -ld "$v"; \ + while :; do \ + v="${v%/*}"; \ + [[ "$v" && ! -f "$v" ]] || break; \ + chown root:root "$v"; \ + done; \ + }; lsd "$NGINX_HOME" \ + && lsd() { \ + local v="$1"; \ + ls -ld $v; \ + while :; do \ + v="${v%/*}"; \ + [[ "$v" && ! -f "$v" ]] || break; \ + chmod 550 "$v"; \ + done; \ + }; lsd $NGINX_HOME \ + && lsd() { \ + local v="$1"; \ + ls -ld $v; \ + while :; do \ + v="${v%/*}"; \ + [[ "$v" && ! -f "$v" ]] || break; \ + chown $NGINX_USER:$NGINX_GROUP "$v"; \ + done; \ + }; lsd $NGINX_HOME \ + && rm -rf /usr/share/nginx/html/ \ + && rm -rf /usr/share/nginx/logs/ \ + && echo "umask 0027" >> /etc/bashrc \ + && echo "set +o history" >> /etc/bashrc \ + && sed -i "s|HISTSIZE=1000|HISTSIZE=0|" /etc/profile \ + && sed -i "s/PASS_MAX_DAYS.*/PASS_MAX_DAYS 30/" /etc/login.defs \ + && echo "ALWAYS_SET_PATH yes" >> /etc/login.defs \ + && chage --maxdays 30 nginx \ + && passwd -l $NGINX_USER \ + && yum clean all \ + && usermod -s /sbin/nologin sync \ + && usermod -s /sbin/nologin shutdown \ + && usermod -s /sbin/nologin halt \ + && echo "export TMOUT=1800 readonly TMOUT" >> /etc/profile \ + && rm -rf /usr/bin/gdb* \ + && rm -rf /usr/share/gdb \ + && rm -rf /usr/share/gcc* \ + && rm -rf /usr/lib64/python3.11/bdb.py \ + && rm -rf /usr/lib64/python3.11/pdb.py \ + && rm -rf /usr/lib64/python3.11/timeit.py \ + && rm -rf /usr/lib64/python3.11/trace.py \ + && rm -rf /usr/lib64/python3.11/tracemalloc.py \ + && rm -rf /usr/share/licenses/glibc \ + && rm -rf /usr/share/locale/ar \ + && rm -rf /usr/share/locale/cpp \ + && yum remove gdb-gdbserver findutils passwd shadow -y + +RUN chmod 500 /etc/nginx/monitor.sh \ + && chmod 500 /etc/nginx/entrypoint.sh \ + && chown nginx:nginx /etc/nginx/monitor.sh \ + && chown nginx:nginx /etc/nginx/entrypoint.sh \ + && sed -i "/PATH=/d" /home/nginx/.bashrc \ + && source /home/nginx/.bashrc + +EXPOSE 8080 + +USER nginx + +ENTRYPOINT ["/etc/nginx/entrypoint.sh"] \ No newline at end of file diff --git a/deploy/entrypoint.sh b/deploy/entrypoint.sh new file mode 100644 index 0000000000000000000000000000000000000000..c0ccd9e42256ccb96c56194422e84f05c16d4575 --- /dev/null +++ b/deploy/entrypoint.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +# 使用 ifconfig 获取主机的 IP 地址(假设是 eth0 接口) +LOCAL_IP=$(ifconfig eth0 | grep inet | awk '{ print $2 }' | head -n 1) + +# 使用 awk 替换 nginx.conf.template 中的环境变量 +echo "Replacing LOCAL_IP in nginx.conf" +awk -v ip="$LOCAL_IP" '{gsub(/\${LOCAL_IP}/, ip); print}' /etc/nginx/nginx.conf.template > /etc/nginx/nginx.conf +bash /etc/nginx/monitor.sh $DET_URL $DST_PATH & +/usr/share/nginx/sbin/nginx -g 'daemon off;' \ No newline at end of file diff --git a/deploy/monitor.sh b/deploy/monitor.sh new file mode 100644 index 0000000000000000000000000000000000000000..d84c1bc8aa3a6504483d3d8879244e142708b2e9 --- /dev/null +++ b/deploy/monitor.sh @@ -0,0 +1,33 @@ +#!/bin/bash +# this script is for website monitoring, +# when website is up, delete all cert file. + +HOST=$1 +DST_PATH=$2 + +delete_file() { + if [ -d $DST_PATH ]; then + echo "found $DST_PATH" > /dev/stdout + rm -rf $DST_PATH/* + else + echo "$DST_PATH not found" > /dev/stdout + fi +} + +while true; +do + sleep 20 + RET=$(curl -k -s -w "%{http_code}\n" -o /dev/null $HOST) + if [ $RET == "200" ]; then + echo "website is up!!!" > /dev/stdout + delete_file + if [ $? -eq 0 ]; then + echo "successful delete file, exit" > /dev/stdout + break + else + echo "failed to delete file" > /dev/stdout + fi + else + echo "waiting for website up, http_status: $RET" > /dev/stdout + fi +done \ No newline at end of file diff --git a/deploy/nginx/nginx.conf b/deploy/nginx/nginx.conf new file mode 100644 index 0000000000000000000000000000000000000000..0c2dcc270ae6feca56becd09d2121bd11e6690f1 --- /dev/null +++ b/deploy/nginx/nginx.conf @@ -0,0 +1,118 @@ +user $NGINX_USER; +error_log /dev/stdout info; +pid /var/run/nginx.pid; +load_module /etc/nginx/modules/ngx_http_geoip2_module.so; +worker_processes auto; +worker_rlimit_nofile 65535; +events { + use epoll; + worker_connections 65535; +} + +http { + include /etc/nginx/mime.types; + + log_format main '[$time_local] remote_addr: $http_true_client_ip, request: "$request", ' + 'status: $status, body_bytes_sent: $body_bytes_sent, http_referer: "$http_referer", ' + 'http_user_agent: "$http_user_agent", ' + '"geoip2_city_country_code": "$geoip2_city_country_code", ' + '"geoip2_city_country_name": "$geoip2_city_country_name", ' + '"geoip2_city": "$geoip2_city"'; + + access_log /dev/stdout main; + geoip2 /etc/nginx/geoip/GeoLite2-Country.mmdb { + $geoip2_city_country_code source=$http_true_client_ip country iso_code; + $geoip2_city_country_name source=$http_true_client_ip country names en; + } + + geoip2 /etc/nginx/geoip/GeoLite2-City.mmdb { + $geoip2_city source=$http_true_client_ip city names en; + } + server_tokens off; + autoindex off; + + port_in_redirect off; + absolute_redirect off; + + client_header_buffer_size 1k; + large_client_header_buffers 4 8k; + client_body_buffer_size 16k; + client_max_body_size 50m; + + client_header_timeout 10; + client_body_timeout 10; + client_body_in_file_only off; + keepalive_timeout 10 30; + send_timeout 10; + + proxy_hide_header X-Powered-By; + + + limit_conn_zone $http_x_real_ip zone=limitperip:10m; + limit_req_zone $http_x_real_ip zone=frontendratelimit:10m rate=2000r/s; + limit_req_zone $http_x_real_ip zone=ratelimit:10m rate=200r/s; + underscores_in_headers on; + + gzip on; + gzip_min_length 1k; + gzip_buffers 4 16k; + gzip_comp_level 5; + gzip_types text/plain application/x-javascript text/css application/xml text/javascript application/javascript application/x-httpd-php application/json; + gzip_vary on; + + server { + listen ${LOCAL_IP}:8080 ssl; + server_name opendesign.osinfra.cn opendesign.test.osinfra.cn; + charset utf-8; + limit_conn limitperip 10; + ssl_session_tickets off; + ssl_session_timeout 10s; + ssl_session_cache shared:SSL:10m; + + ssl_certificate "cert/server.crt"; + ssl_certificate_key "cert/server.key"; + ssl_password_file "cert/abc.txt"; + ssl_dhparam "cert/dhparam.pem"; + ssl_ecdh_curve auto; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers "ECDHE-RSA-AES256-GCM-SHA384"; + ssl_prefer_server_ciphers on; + ssl_stapling on; + ssl_stapling_verify on; + resolver 8.8.8.8 8.8.4.4 valid=60s; + resolver_timeout 5s; + + if ($request_method !~ ^(GET|HEAD|POST)$) { + return 444; + } + + location ~ /\. { + deny all; + return 404; + } + + location / { + add_header X-XSS-Protection "1; mode=block"; + add_header X-Frame-Options DENY; + add_header X-Content-Type-Options nosniff; + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains"; + add_header Content-Security-Policy "script-src 'self' 'unsafe-inline' 'unsafe-eval' data:; frame-src; worker-src 'self' blob:"; + add_header Cache-Control "no-cache,no-store,must-revalidate"; + add_header Pragma no-cache; + add_header Expires 0; + + location /assets { + add_header X-XSS-Protection "1; mode=block"; + add_header X-Frame-Options DENY; + add_header X-Content-Type-Options nosniff; + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains"; + add_header Content-Security-Policy "script-src 'self' 'unsafe-inline' 'unsafe-eval'; object-src 'none'; frame-src 'none'"; + add_header Cache-Control "public,max-age=1209600"; + } + + root /usr/share/nginx/www; + index index.html index.htm; + try_files $uri $uri/ /index.html; + } + } +} diff --git a/package.json b/package.json index 1dd07dd68b5628de544d5f8e90f8357a23521347..78b31dd4c9948da257dfd007e25aa6c2df0c7b1d 100644 --- a/package.json +++ b/package.json @@ -5,21 +5,25 @@ "license": "MIT", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", - "dev": "pnpm --filter portal dev", - "dev:ak": "pnpm --filter portal-ak dev", - "dev:lib": "pnpm --filter @opensig/opendesign dev" + "dev": "pnpm -C packages/portal dev", + "docs:install": "pnpm i && pnpm -C packages/scripts build && pnpm i && pnpm -C packages/opendesign gen:icon && pnpm -C packages/docs gen:icon && pnpm -C packages/docs gen:api ", + "docs:dev": "pnpm -C packages/docs dev", + "docs:build": "pnpm -C packages/docs build" }, "dependencies": { - "normalize.css": "^8.0.1", - "vue": "^3.5.13", - "vue-router": "^4.5.0" + "normalize.css": "catalog:css", + "vue": "catalog:vue", + "vue-router": "catalog:vue" }, "devDependencies": { - "@rushstack/eslint-patch": "^1.10.5", - "@types/node": "^22.13.1", - "@vitejs/plugin-vue": "^5.2.1", - "@vue/eslint-config-typescript": "^12.0.0", - "eslint": "^8.57.1", - "eslint-plugin-vue": "^9.32.0" + "@rushstack/eslint-patch": "catalog:lint", + "@types/node": "catalog:typescript", + "@vitejs/plugin-vue": "catalog:vue", + "@vue/eslint-config-typescript": "catalog:lint", + "eslint": "catalog:lint", + "eslint-plugin-vue": "catalog:lint", + "typescript": "catalog:typescript", + "vite-node": "catalog:utils", + "vue-eslint-parser": "catalog:lint" } } diff --git a/packages/docs/.gitignore b/packages/docs/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..bea45e6a6b805ca05c1895a16ac2919b087edfe8 --- /dev/null +++ b/packages/docs/.gitignore @@ -0,0 +1,29 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +icon-components +es +lib +src/router/components.ts diff --git a/packages/docs/README.md b/packages/docs/README.md new file mode 100644 index 0000000000000000000000000000000000000000..ecf6d1b90822ab865e02551efa9100a06df8c07a --- /dev/null +++ b/packages/docs/README.md @@ -0,0 +1,59 @@ +# opendesign 组件文档 + +## 快速开始 + +### 📦 安装依赖 +```bash +pnpm docs:install +``` +> 会自动完成以下操作: +> 1. 安装项目依赖 +> 2. 编译 opendesign 组件 +> 3. 生成 API 文档 + +### 🚀 运行开发服务 +```bash +pnpm docs:dev +``` +浏览器访问:http://localhost:3300 + +### 🛠️ 构建生产环境 +```bash +pnpm docs:build +``` + +## 🌍 英文翻译 + +1. **Fork 仓库** 到个人账号 +2. **创建特性分支** `git checkout -b feat/translate` +3. **新增语言文件** + ```bash + # 按以下结构创建文件 + /packages/opendesign/src/*/__docs__/index.en-US.md # 语言主文档 + /packages/opendesign/src/*/__docs__/*-api.en-US.md # API 文档 + ``` +4. **demo组件英文翻译** 在 `/packages/opendesign/src/*/__docs__/__case__/*.vue` 文件中的`` 标签中添加en-US语言配置 + + ```html + + + + ### 中文标题 + + 中文内容 + + + + ### English Title + + English content here + + ``` +5. 提交 PR 并等待审核 +6. 维护人员审核通过后合并 + +## ⚠️ 注意事项 +- 当前支持语言:`zh-CN`(简体中文) 和 `en-US`(英语) +- 扩展语言需遵循命名规范:`-.md`(如 `es-US`) +- 保持原始文件结构,确保国际化文件位置正确 +- 修改内容时请勿删除原有语言版本 diff --git a/packages/docs/ReleaesNote.opendesign.md b/packages/docs/ReleaesNote.opendesign.md index bef1bf6446fb58dfaa8012169ce14924ecc2bdfe..fc5d063f6d6405687198575b7969349f216626b9 100644 --- a/packages/docs/ReleaesNote.opendesign.md +++ b/packages/docs/ReleaesNote.opendesign.md @@ -12,23 +12,73 @@ --- +# 1.0.2 + +- feat + +1. [cascader] cascader新增expandTrigger参数,支持hover触发展开下一级菜单 +2. [button] 新增 `--btn-gap-prefix` 及 `--btn-gap-suffix` 变量以单独控制前缀及后缀图标外边距 +3. [scss-mixin] respond-to新增断点 +4. [menu] 菜单增加small尺寸 +5. [figure] 新增CSS变量--figure-error-bk控制图片加载失败时背景色 + +- fix + +1. [cascader] 修复--cascader-options-bd-clor单词拼写错误 +2. [cascader] 修复trigger参数失效,数据回显bug;重构优化CascaderTree类 +3. [breadcrumb] 调整组件hover active状态颜色 +4. [breadcrumb] 将 `--breadcrumb-seperator-size` 修改为 `--breadcrumb-separator-size` +5. [badge] 修复OBadge组件offset-x不支持负数 +6. [rate] 完善ORate组件icon插槽status变量类型签名 +7. [select] 优化select组件provide的select函数 +8. [vScrollbar] 优化类型签名 +9. [figure] 修复组件lazyPreview参数单词拼写错误 +10. [figure] 修复当 background 为真时图片懒加载完成前不能通过 ratio 属性保持宽高比的问题 +11. [figure] 修复组件在设置 background 为 true 且未设置 ratio 属性时 + - background 不显示 + - load 或 error 触发两次 + - 组件的高度应该通过默认插槽中的内容撑开 + +# 1.0.1 + +- feat + +1. 重构多语言,增加词条可读性;支持useI18n在非组件中使用; +2. [utils]添加分批执行函数,可用于耗时的大量任务执行 + +# 1.0.0 + +- feat + +1. [scrollbar] 支持监听内部唯一子元素尺寸改变 +2. [collapse] 重构 Collapse,支持受控模式 +3. [input] 新增对外接口:focus,blur,clear,togglePassword,inputEl +4. [textarea] 新增暴露 focus、blur、clear 接口; + +- fix + +1. [input] 阻止点击眼睛图标的事件冒泡,修复密码框在切换显示密码时,与外层 popover 显示逻辑冲突问题; +2. [textarea] 修复 limit 样式错误;limit 文本在圆角为 pill 时文本溢出问题; +3. [input/textarea] 修复 OInput 及 OTextarea 在发送验证码读秒等场景中,无法使用输入法输入中文内容的 bug +4. [link] 保证基线对其的情况下实现图标对齐 +5. [input-number] 修复按钮宽度 +6. [figure] 优化默认播放图标 + # 0.0.78 - fix -1. [common] 使用线上@opensig/open-scripts进行构建 +1. [common] 使用线上@opensig/open-scripts 进行构建 # 0.0.77 - 无 - # 0.0.76 - fix -1. [input/textarea] 修复点击清除按钮后,再点击其他区域无法失焦的问题;修复点击prepend和append区域无法触发失焦的问题 - +1. [input/textarea] 修复点击清除按钮后,再点击其他区域无法失焦的问题;修复点击 prepend 和 append 区域无法触发失焦的问题 # 0.0.75 diff --git a/packages/docs/ReleaesNote.scripts.md b/packages/docs/ReleaesNote.scripts.md index 27c9f58ed70432d6327b638d503f7fde511b81d6..84f0a91513fc8512e4ee1dbd48a46930d4169c8d 100644 --- a/packages/docs/ReleaesNote.scripts.md +++ b/packages/docs/ReleaesNote.scripts.md @@ -1,5 +1,11 @@ # [open-scripts] +# 1.0.0 + +- fix + +1. 统一gen-icon在不同系统下生成path的分隔符风格 + # 0.0.23 - fix diff --git a/packages/docs/badge.md b/packages/docs/badge.md deleted file mode 100644 index 42323c182d924d22f178d3687186beed0f0e65b8..0000000000000000000000000000000000000000 --- a/packages/docs/badge.md +++ /dev/null @@ -1,18 +0,0 @@ -# Badge 徽标 - -## props - -| name | type | 默认值 | 说明 | -| :----- | :-------------------------------------------------------- | :------- | -------------------------------------------------------------------- | -| value | string \| number | '' | 可选,显示值 | -| max | number | 99 | 可选,最大值,超过最大值显示${max}+(仅当 value 类型为 number 时生效) | -| offset | number [] | [] | 可选,徽标位置偏移量 | -| dot | boolean | false | 可选,是否显示为小红点 | -| color | 'normal' \| 'primary' \|'success'\| 'warning' \| 'danger' | 'normal' | 可选,颜色 | - -## slot - -| name | 参数 | 说明 | -| :------ | :--- | :----------- | -| default | | 徽标包裹内容 | -| content | | 徽标具体内容 | diff --git a/packages/docs/breadcrumb.md b/packages/docs/breadcrumb.md deleted file mode 100644 index df24bd21de7d899a1871ced5258cc37f925c2668..0000000000000000000000000000000000000000 --- a/packages/docs/breadcrumb.md +++ /dev/null @@ -1,15 +0,0 @@ -# Breadcrumb 面包屑 - -| name | type | 默认值 | 说明 | -| :-------- | :--------------- | :----- | ---------------- | -| separator | string \| number | - | 可选,分隔符字符 | - -# BreadcrumbItem - -| name | type | 默认值 | 说明 | -| :-------- | :--------------------------------------------- | :------- | ---------------------------------------- | -| href | string | - | 可选,链接跳转地址 | -| target | '\_blank' \| '\_parent' \| '\_self' \| '\_top' | '\_self' | 可选,链接跳转方式 | -| to | string \| object | - | 可选,路由跳转对象,同 vue-router | -| replace | boolean | false | 可选,路由跳转时,是否覆盖浏览器历史记录 | -| separator | string \| number | - | 可选,分隔符字符 | diff --git a/packages/docs/button.md b/packages/docs/button.md deleted file mode 100644 index bc5a8fe86401d0cbfb3497e7130a11f87f2938fa..0000000000000000000000000000000000000000 --- a/packages/docs/button.md +++ /dev/null @@ -1,38 +0,0 @@ -# Button 按钮 组件设计 - -# 组件介绍 - -页面中常用的按钮 - -1. 支持设置不同状态颜色,设置边框、填充样式,以及圆角值(也支持全局设置); -2. 支持加载状态 -3. 支持设置链接跳转,图标 - -## props 入参 - -| name | type | 默认值 | 说明 | -| :------- | :---------------------------------------------------------- | :-------- | :------------------------- | -| color | "normal" \| "primary" \| "success" \| "warning" \| "danger" | 'normal' | 可选,颜色类型 | -| size | 'mini' \| 'small' \| 'medium' \| 'large' | 'medium' | 可选,按钮尺寸 | -| variant | "solid" \| "outline" \| "text" | 'outline' | 可选,按钮类型 | -| loading | boolean | false | 可选,加载状态 | -| disabled | boolean | false | 可选,是否为禁用状态 | -| href | string | -- | 可选,链接跳转 | -| icon | Component | -- | 可选,前缀图标 | -| tag | string | 'button' | 可选,自定义元素 html 标签 | - -## event 事件 - -| name | 参数 | 返回值 | 说明 | -| :---- | :------------- | :----- | :------- | -| click | (e:MouseEvent) | -- | 点击事件 | - -## slot 插槽 - -| name | 说明 | -| :------ | :----------- | -| default | 按钮内容 | -| icon | 前缀按钮图标 | -| suffix | 后缀 | - -## expose 导出 diff --git a/packages/docs/checkbox.md b/packages/docs/checkbox.md deleted file mode 100644 index 92fb834678b73a2a2b5a4724167d3e9be38167f9..0000000000000000000000000000000000000000 --- a/packages/docs/checkbox.md +++ /dev/null @@ -1,55 +0,0 @@ -# Checkbox 多选框 - -## props - -| name | type | 默认值 | 说明 | -| :------------------ | :------------------------ | :----- | -------------------------------- | -| value | string \| number | - | 必选,多选框 value | -| modelValue(v-model) | Array | - | 可选,多选框双向绑定值 | -| defaultChecked | boolean | false | 可选,非受控状态时,默认是否选中 | -| disabled | boolean | false | 可选,是否禁用 | -| indeterminate | boolean | false | 可选,是否为半选状态 | - -## event - -| name | 参数 | 说明 | -| :----- | :------------------------------ | :----------------------------------- | -| change | val: Array; | 双向绑定值改变时,在选中多选框上触发 | - -## expose - -| name | type | 说明 | -| :------ | :------ | :------------- | -| checked | boolean | 多选框是否选中 | - -## slot - -| name | 参数 | 说明 | -| :------- | :--------------------------------- | :------------- | -| checkbox | checked:boolean; disabled: boolean | 自定义多选框 | -| default | | 多选框文字内容 | - -# CheckboxGroup 多选框组 - -## props - -| name | type | 默认值 | 说明 | -| :------------------ | :------------------------ | :----- | -------------------------------------- | -| modelValue(v-model) | Array | - | 可选,多选框组双向绑定值 | -| defaultValue | Array | [] | 可选,非受控状态时,多选框组默认值 | -| disabled | boolean | false | 可选,是否禁用 | -| direction | 'h' \| 'v' | 'h' | 可选,排列方向 | -| min | number | - | 可选,多选框组支持选中的最小多选框数量 | -| max | number | - | 可选,多选框组支持选中的最大多选框数量 | - -## event - -| name | 参数 | 说明 | -| :----- | :----------------------------- | :------------- | -| change | val: Array | 状态切换后触发 | - -## slot - -| name | 参数 | 说明 | -| :------ | :--- | :----------- | -| default | | 多选框组内容 | diff --git a/packages/docs/divider.md b/packages/docs/divider.md deleted file mode 100644 index 66af2e549e102148a54c910c0e4df3a2157daf79..0000000000000000000000000000000000000000 --- a/packages/docs/divider.md +++ /dev/null @@ -1,15 +0,0 @@ -# Divider 分割线 - -## props - -| name | type | 默认值 | 说明 | -| :------------ | :------------------------------ | :------- | -------------------- | -| variant | 'solid' \| 'dashed' \| 'dotted' | 'solid' | 可选,分割线类型 | -| direction | 'h' \| 'v' | 'h' | 可选,分割线方向 | -| labelPosition | 'left' \| 'center' \| 'right' | 'center' | 可选,自定义内容位置 | - -## slot - -| name | 参数 | 说明 | -| :------ | :--- | :------------- | -| default | | 分割线文字内容 | diff --git a/packages/docs/global.md b/packages/docs/global.md deleted file mode 100644 index ee01ad06970e9817722a5f5e9c8ae5e3e77688d4..0000000000000000000000000000000000000000 --- a/packages/docs/global.md +++ /dev/null @@ -1,39 +0,0 @@ -# 全局配置设计 - -组件可以通过全局配置部分样式属性,同时支持动态设置。 - -## 方案设计 - -1. 设置全局默认值,同时暴露接口进行默认值修改,基于 vue 响应式,实现全局配置动态化 -2. 组件内部图标统一配置,并支持接口修改,实现组件深度定制 - -## 配置全局样式 - -| 方法名 | 参数 | 返回值 | 说明 | -| :------------- | :-------------------------------------- | :----- | :----------------------- | -| initSize | (type: 'large' \| 'medium' \| 'small') | -- | 设置全局组件尺寸 | -| initRound | (type: 'pill' \| '') | -- | 设置全局组件圆角 | -| initZIndex | (val: number) | -- | 设置全局组件初始 z-index | -| initMediaPoint | point: Record<'phone' \| 'pad', number> | -- | 设置全局组件响应式断点 | - -## 配置全局图标 - -具体接口见[`init-icons.ts`](../opendesign/src/_utils/init-icons.ts) - -# 变量定义都在 var.scss 里,同时使用最外层内定义; - -# 状态类 如果在最外层使用 o-[component]-[status],在内部可以使用 is-[status] - -# 不同状态通过类改变变量值,而不是新定义多个变量; - -# 变量命名 - -- 边框(bd) `--btn-bd: 1px solid var(--o-color-control1)` -- 边框颜色(bd-color) `--btn-bd-color: var(--o-color-control1)` -- 背景颜色(bg-color) `--btn-bg-color: var(--o-color-control1)` - -# polyfill - -> resize-observer-polyfill -> intersection-observer-polyfill -> 建议在项目使用时引入 diff --git a/packages/docs/helper/utils.ts b/packages/docs/helper/utils.ts new file mode 100644 index 0000000000000000000000000000000000000000..cb728f93c9f9b1d6254497fe4f26085becdac27b --- /dev/null +++ b/packages/docs/helper/utils.ts @@ -0,0 +1,80 @@ +import { type SFCBlock } from '@vue/compiler-sfc'; +export function getLangByFileName(_fileName: string) { + const fileName = _fileName.slice(_fileName.lastIndexOf('/') + 1); + const [name, lang, ext] = fileName.split('.'); + if (!ext) { + return { name, lang: '', ext: lang }; + } + return { name, lang, ext }; +} + +export function generateCode(block: SFCBlock) { + return `<${block.type}${Object.entries(block.attrs) + .map(([key, value]) => { + if (typeof value === 'string') { + return ` ${key}="${value}"`; + } else { + return ` ${key}`; + } + }) + .join('')}>${block.content}\n`; +} + +/** + * 解析vue文件的自定义块docs + * @param code 带解析的md代码 + * @returns 解析产物 + */ +export function parseDocsCode(code: string) { + const langSeparator = //gm; + const langMatchList = Array.from(code.matchAll(langSeparator)); + return langMatchList.map((langMatch, matchIndex) => { + const lang = langMatch[1]; + const langCode = code.slice( + langMatch.index + langMatch[0].length, + matchIndex === langMatchList.length - 1 ? code.length : langMatchList[matchIndex + 1].index, + ); + return { + lang, + code: langCode, + }; + }); +} + +export function escapeRegExp(str: string) { + return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} + +/** + * 异步替换字符串中的匹配项 + * @param str - 原始字符串 + * @param regex - 正则表达式或字符串形式的正则,若想全局替换,请设置正则表达式的g标志 + * @param asyncReplacer - 异步替换函数 + * @returns 处理后的字符串 + */ +export async function asyncReplace( + str: string, + regex: RegExp | string, + asyncReplacer: (matched: RegExpExecArray) => Promise | string, +): Promise { + const finalRegex = typeof regex === 'string' ? new RegExp(escapeRegExp(regex)) : regex; + + const matches = finalRegex.global ? Array.from(str.matchAll(finalRegex)) : [finalRegex.exec(str)].filter(Boolean); + const replacements = await Promise.all(matches.map((matched) => asyncReplacer(matched))); + + let lastIndex = 0; + let result = ''; + + matches.forEach((matched, i) => { + // 添加非匹配内容 + result += str.slice(lastIndex, matched.index); + // 添加替换内容 + result += replacements[i]; + // 正确更新最后索引位置 + lastIndex = matched.index + matched[0].length; + }); + + // 添加剩余内容 + result += str.slice(lastIndex); + return result; +} diff --git a/packages/docs/helper/vue-utils.ts b/packages/docs/helper/vue-utils.ts new file mode 100644 index 0000000000000000000000000000000000000000..f3bcb2be691648f0e3a516000ed0c4cef6ca1667 --- /dev/null +++ b/packages/docs/helper/vue-utils.ts @@ -0,0 +1,108 @@ +import { parse, NodeTypes, type ElementNode } from '@vue/compiler-dom'; +import { type SFCDescriptor, type SFCBlock, type SFCStyleBlock, type SFCTemplateBlock, type SFCScriptBlock } from '@vue/compiler-sfc'; + +function hasSrc(node: ElementNode) { + return node.props.some((p) => { + if (p.type !== NodeTypes.ATTRIBUTE) { + return false; + } + return p.name === 'src'; + }); +} + +function isEmpty(node: ElementNode) { + for (let i = 0; i < node.children.length; i++) { + const child = node.children[i]; + if (child.type !== NodeTypes.TEXT || child.content.trim() !== '') { + return false; + } + } + return true; +} + +function createBlock(node: ElementNode, source: string): SFCBlock { + const type = node.tag; + const loc = node.innerLoc!; + const attrs: Record = {}; + const block: SFCBlock = { + type, + content: source.slice(loc.start.offset, loc.end.offset), + loc, + attrs, + }; + node.props.forEach((p) => { + if (p.type === NodeTypes.ATTRIBUTE) { + const name = p.name; + attrs[name] = p.value ? p.value.content || true : true; + if (name === 'lang') { + block.lang = p.value && p.value.content; + } else if (name === 'src') { + block.src = p.value && p.value.content; + } else if (type === 'style') { + if (name === 'scoped') { + (block as SFCStyleBlock).scoped = true; + } else if (name === 'module') { + (block as SFCStyleBlock).module = attrs[name]; + } + } else if (type === 'script' && name === 'setup') { + (block as SFCScriptBlock).setup = attrs.setup; + } + } + }); + return block; +} + +const sfcCache = new Map(); +// 由于 @vue/compiler-sfc 包体积过大,且当前只需要使用其 parse 函数,因此创建一个简单的 parse 函数 +// 该函数省略了报错,仅返回解析结果 +export function parseSfc(source: string) { + const cache = sfcCache.get(source); + if (cache) { + return cache; + } + const descriptor: Pick = { + template: null, + script: null, + scriptSetup: null, + styles: [], + customBlocks: [], + }; + + const ast = parse(source, { + parseMode: 'sfc', + prefixIdentifiers: true, + }); + + ast.children.forEach((node) => { + if (node.type !== NodeTypes.ELEMENT) { + return; + } + if (node.tag !== 'template' && isEmpty(node) && !hasSrc(node)) { + return; + } + switch (node.tag) { + case 'template': + descriptor.template = createBlock(node, source) as SFCTemplateBlock; + break; + case 'script': { + const scriptBlock = createBlock(node, source) as SFCScriptBlock; + if (scriptBlock.attrs.setup) { + descriptor.scriptSetup = scriptBlock; + } else { + descriptor.script = scriptBlock; + } + break; + } + case 'style': { + const styleBlock = createBlock(node, source) as SFCStyleBlock; + descriptor.styles.push(styleBlock); + break; + } + default: + descriptor.customBlocks.push(createBlock(node, source)); + break; + } + }); + sfcCache.set(source, descriptor); + return descriptor; +} diff --git a/packages/docs/i18n.md b/packages/docs/i18n.md deleted file mode 100644 index 9a4f3ae9eb7757ab2683178d02b7c3c96b1eee90..0000000000000000000000000000000000000000 --- a/packages/docs/i18n.md +++ /dev/null @@ -1,51 +0,0 @@ -# opendesign 组件国际化方案设计 - -## 背景分析 - -当前组件存在内置词条,如下拉的 placeholder、分页器的页码等,在支持多语言的网站(如 openMind)中,组件也需要默认支持语言,但当前组件库还不支持; - -## 需求分析 - -1. 组件库需要默认内置多语言类型及词条; -2. 组件库支持开发者自定义语言类型及对应词条; -3. 组件库支持全局设置语言类型,也支持模块级设置语言类型; -4. 每个组件能根据语言切换动态切换词条 - -## 实现方案设计 - -1. 组件库需要默认内置多语言类型及词条; - -- 组件库默认支持中英文,并提供中英文词条; - -2. 组件库支持开发者自定义语言类型及对应词条; - -- 通过接口支持配置自定义语言类型及词条; - -3. 组件库支持全局设置语言类型,也支持模块级设置语言类型; - -- 通过注入的方式,实现全局及部分模块语言设置; - -4. 每个组件能根据语言切换动态切换词条 - -- 组件中所有词条为响应式数据,使用统一接口设置; - -## API - -- 全局函数:导出 `useLocale`、`addLocale`,支持全局设置语言类型 - | function | 参数 | 说明 | - | :----- | :-------- | ----------- | - | useLocale | localeKey: string | 设置全局使用的语言类型 | - | addLocale | locale: i18nLanguages, opts?: { overwrite?: boolean;} | 全局增加语言类型 | - -- 组件**ConfigProvider** - | name | type | 默认值 | 说明 | - | :----- | :-------- | :-- | ----------- | - | locale | LanguageT | {} | 可选,设置语言 | - -## 涉及组件 - -- 下拉选择器 Select -- 分页器 Pagination -- 表格 Table -- 上传 Upload -- 输入框 Input diff --git a/packages/docs/icons/icon.config.ts b/packages/docs/icons/icon.config.ts new file mode 100644 index 0000000000000000000000000000000000000000..b816a5860ed547b3b8d5e3499816be943b6c8fe7 --- /dev/null +++ b/packages/docs/icons/icon.config.ts @@ -0,0 +1,6 @@ +module.exports = { + input: './svgs', + output: '../src/icon-components/', + componentClass: 'o-svg-icon', + prefix: 'doc-', +}; diff --git a/packages/docs/icons/svgs/fill/code.svg b/packages/docs/icons/svgs/fill/code.svg new file mode 100644 index 0000000000000000000000000000000000000000..1862aa9fd34645236e72a7b918999214649f10a9 --- /dev/null +++ b/packages/docs/icons/svgs/fill/code.svg @@ -0,0 +1,4 @@ + + + \ No newline at end of file diff --git a/packages/docs/icons/svgs/fill/copy.svg b/packages/docs/icons/svgs/fill/copy.svg new file mode 100644 index 0000000000000000000000000000000000000000..e549a76b6c52acef178d1536aff585888824b00a --- /dev/null +++ b/packages/docs/icons/svgs/fill/copy.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/packages/docs/icons/svgs/fill/dark.svg b/packages/docs/icons/svgs/fill/dark.svg new file mode 100644 index 0000000000000000000000000000000000000000..61997a54e9da4f9807d379ceee81de89d79bbf9e --- /dev/null +++ b/packages/docs/icons/svgs/fill/dark.svg @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/packages/docs/icons/svgs/fill/light.svg b/packages/docs/icons/svgs/fill/light.svg new file mode 100644 index 0000000000000000000000000000000000000000..7f966ca3016245d1683b3133629a48837b873b04 --- /dev/null +++ b/packages/docs/icons/svgs/fill/light.svg @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/packages/docs/icons/svgs/fill/menu.svg b/packages/docs/icons/svgs/fill/menu.svg new file mode 100644 index 0000000000000000000000000000000000000000..49c3c0c52ff8f4e8090d206231425f926f82946c --- /dev/null +++ b/packages/docs/icons/svgs/fill/menu.svg @@ -0,0 +1,4 @@ + + + \ No newline at end of file diff --git a/packages/docs/icons/svgs/fill/out-link.svg b/packages/docs/icons/svgs/fill/out-link.svg new file mode 100644 index 0000000000000000000000000000000000000000..ffe90d5cfac7c70be9a44f70074df2fb7b47c466 --- /dev/null +++ b/packages/docs/icons/svgs/fill/out-link.svg @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/packages/docs/icons/svgs/fill/pwd.svg b/packages/docs/icons/svgs/fill/pwd.svg new file mode 100644 index 0000000000000000000000000000000000000000..387f1a27609508906259639a62ad54319ad1f38a --- /dev/null +++ b/packages/docs/icons/svgs/fill/pwd.svg @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/packages/portal-ak/index.html b/packages/docs/index.html similarity index 44% rename from packages/portal-ak/index.html rename to packages/docs/index.html index cbbcf83965dafd1dca9d52038bf8af5e3c9c46fd..693d3e60ee2b84eeb4746fe8f7cbfa7f3e0ec33b 100644 --- a/packages/portal-ak/index.html +++ b/packages/docs/index.html @@ -1,16 +1,10 @@ - + - + - opendesign-AK - + opendesign
diff --git a/packages/docs/input-number.md b/packages/docs/input-number.md deleted file mode 100644 index fc6ba92b65c520b7776e7613952e2e6efac08ebe..0000000000000000000000000000000000000000 --- a/packages/docs/input-number.md +++ /dev/null @@ -1,38 +0,0 @@ -# input-number 数字输入框 - -## 说明 - -> currentValue: number|string 数字输入框当前值 - -> realValue: number 数字输入框当前真实值 - -> inputValue: string 输入框框当前值 - -> 数字非法判断:非 number 字符串(Number 无法转换为数字)、不在[min, max]范围内; - -## 设计 - -1. 初始化 currentValue - - `currentValue = modelValue ?? defaultValue` - -- 如果 modelValue 非法,展示该值,并使用删除线样式 -- 如果 defaultValue 非法,展示该值,并使用删除线样式 - -2. 聚焦时(鼠标点击,键盘 Tab 切换) - -- 触发 focus 事件,参数:(realValue, event) - -3. 输入时 - -- 触发`input`事件,参数:(inputValue, event) -- 如果输入值非法,展示该值,并使用删除线样式 - -4. 失焦时 - -- -- 触发`blur`事件,参数:(currentValue, event) - -parse/format - -1. 当 change 事件时,执行 format diff --git a/packages/docs/input.md b/packages/docs/input.md deleted file mode 100644 index 6ac8db65e1cf6049645e68eac8e3336dde79951d..0000000000000000000000000000000000000000 --- a/packages/docs/input.md +++ /dev/null @@ -1,91 +0,0 @@ -# Input 输入框 - -输入框组件,支持用户输入内容 - -## 说明 - -> `realValue: number|string` 数字输入框当前值 - -> `inputValue: string` 输入框框当前值 - -> `displayValue: string` 输入框展示的值 - -> 数字非法判断:非 number 字符串(Number 无法转换为数字)、不在[min, max]范围内; - -## 组件交互设计 - -1. 初始化 realValue - - `realValue = modelValue ?? defaultValue` - -- 如果 `modelValue` 非法,展示该值,并使用删除线样式; -- 如果 `defaultValue` 非法,展示该值,并使用删除线样式; - -2. 聚焦时(鼠标点击,键盘 Tab 切换) - -- 触发 `focus` 事件,参数:`(realValue, e)`; - -3. 输入时 - -- 触发`input`事件,参数:`(inputValue, event)`; -- 如果无 `parse`, 赋值`realValue = inputValue`,触发`update:modelValue`事件,参数`(inputValue)`; - -4. 失焦时 - -- 如果有 `format`, `displayValue = format(realValue)`; -- 如果没有 `format`, `displayValue = realValue`; -- 如果有 `parse`, `realValue = parse(inputValue)`; -- 触发`blur`事件,参数:`(realValue, event)`; -- 如果值改变,触发`change`事件,参数: `(realValue)` -- 触发`update:modelValue`事件,参数: `(realValue)` - -5. Enter 键按下时 - -- 如果有 `format`, `displayValue = format(inputValue)`; -- 如果没有 `format`, `displayValue = inputValue`; -- 触发`pressEnter`事件,参数:`(realValue, KeyboardEvent)`; -- 如果值改变,触发`change`事件,参数: `(realValue)` -- 触发`update:modelValue`事件,参数: `(realValue)` - -## props 入参 - -| name | type | 默认值 | 说明 | -| :------------- | :---------------------------------------------------------- | :---------- | :--------------------------------------------------------------- | -| modelValue | String, Number | -- | 可选,输入框的值 v-model | -| defaultValue | String, Number | -- | 可选,输入框的默认值 | -| type | 'text' \| 'password' | 'text' | 可选,输入框类型 | -| color | "normal" \| "primary" \| "success" \| "warning" \| "danger" | 'normal' | 可选,颜色类型 | -| size | 'mini' \| 'small' \| 'medium' \| 'large' | 'medium' | 可选,按钮尺寸 | -| variant | "solid" \| "outline" \| "text" | 'outline' | 可选,按钮类型 | -| disabled | boolean | false | 可选,是否为禁用状态 | -| readonly | boolean | false | 可选,是否为只读状态 | -| invalid | boolean | false | 可选,是否为非法值状态 | -| clearable | boolean | true | 可选,是否可以清除。 | -| autoWidth | boolean | true | 可选,是否自动增加宽度 | -| placeholder | string | -- | 可选,提示文本 | -| showPasswordOn | 'click' \| 'mousedown' | 'mousedown' | 可选,显示密码的方式 | -| parse | (value: string) => string | -- | 可选,解析输入框的值 | -| format | (value: string \| number) => string \| number | -- | 可选,对值格式化,控制显示格式,需搭配 parse 处理,保证值的正确性 | - -## event 事件 - -| name | 参数 | 返回值 | 说明 | -| :---------------- | :--------------------------------- | :----- | :------------- | -| update:modelValue | (value: string) | -- | 值更新事件 | -| change | (value: string) | -- | 值变化事件 | -| input | (value: string,evt: Event) | -- | 输入事件 | -| blur | (value: string,evt: FocusEvent) | -- | 失焦事件 | -| focus | (value: string,evt: FocusEvent) | -- | 聚焦事件 | -| clear | (evt: Event) | -- | 清除输入框事件 | -| pressEnter | (value: string,evt: KeyboardEvent) | -- | 回车事件 | - -## slot 插槽 - -| name | 说明 | -| :------ | :------- | -| prepend | 前置插槽 | -| prefix | 前缀插槽 | -| suffix | 后缀插槽 | -| append | 后置插槽 | - -## expose 导出 diff --git a/packages/docs/link.md b/packages/docs/link.md deleted file mode 100644 index bb23a5b0c2cd7d05ed23dc595aca0a6d0884916f..0000000000000000000000000000000000000000 --- a/packages/docs/link.md +++ /dev/null @@ -1,28 +0,0 @@ -# Link 按钮 - -## props - -| name | type | 默认值 | 说明 | -| :--------- | :---------------------------------------------------------- | :------- | :--------------------------------------- | -| href | string | '' | 可选,包含超链接指向的 URL 或 URL 片段。 | -| target | '\_blank' \| '\_parent' \| '\_self' \| '\_top' | 'normal' | 可选,指定在何处显示链接的资源。 | -| type | 'normal' \| 'primary' \| 'warning' \| 'danger' \| 'success' | 'normal' | 可选,链接类型 | -| disabled | boolean | false | 可选,是否禁用 | -| loading | boolean | false | 可选,是否为 loading 状态 | -| iconPrefix | boolean | false | 可选,前缀图标 | -| iconArrow | boolean | false | 可选,图标箭头 | -| hoverable | boolean | false | 可选,hover 时是否显示背景 | - -## event - -| event | 参数 | 说明 | -| :---- | :------------- | :------- | -| click | (e:MouseEvent) | 点击事件 | - -## expose - -## slot - -| name | 说明 | -| :--- | :------- | -| icon | 按钮图标 | diff --git a/packages/docs/menu.md b/packages/docs/menu.md deleted file mode 100644 index 3f78af70401f587bf4da15e9791fa2bc8439fd3c..0000000000000000000000000000000000000000 --- a/packages/docs/menu.md +++ /dev/null @@ -1,50 +0,0 @@ -# Menu 菜单 - -## OMenu - -### props - -| name | type | 默认值 | 说明 | -| :------------------ | :-------------- | :----- | -------------------------------------- | -| modelValue(v-model) | string | - | 可选,双向绑定值 | -| defaultValue | boolean | false | 可选,非受控状态时,默认选中值 | -| expanded(v-model) | Array | - | 可选,双向展开的子菜单值 | -| defaultExpanded | Array | false | 可选,非受控状态时,默认展开的子菜单值 | -| accordion | boolean | false | 可选,是否开启手风琴模式 | -| levelIndent | number | 24 | 可选,层级缩进值 | - -### event - -| name | 参数 | 说明 | -| :-------------- | :------------------- | :--------------------- | -| change | val: string | 选中值切换后触发 | -| expanded-change | val: Array | 展开子菜单值切换后触发 | - -## OSubMenu - -### props - -| name | type | 默认值 | 说明 | -| :---- | :----- | :----- | -------------- | -| value | string | - | 必选,子菜单值 | - -## slot - -| name | 参数 | 说明 | -| :--- | :--- | :--------- | -| icon | | 自定义图标 | - -## OMenuItem - -### props - -| name | type | 默认值 | 说明 | -| :------- | :------ | :----- | ---------------- | -| value | string | - | 必选,菜单选项值 | -| disabled | boolean | false | 可选,是否禁用 | - -## event - -| name | 参数 | 说明 | -| :---- | :--- | :------- | -| click | | 点击触发 | diff --git a/packages/docs/name.md b/packages/docs/name.md deleted file mode 100644 index 32a5afc1bbda7a112cdf8998d36b1a24b2aa64b1..0000000000000000000000000000000000000000 --- a/packages/docs/name.md +++ /dev/null @@ -1,12 +0,0 @@ -## 变量命名 - -1. 命名规则: `--o-[模块/组件]-[属性]-[状态]` - -2. 模块、组件、属性、状态内部使用 “\_” 连接 - -> 示例: - -```css ---btn-color-inverse ---o-font_size-display4 -``` diff --git a/packages/docs/opendesign.md b/packages/docs/opendesign.md deleted file mode 100644 index db1c66efcd2848f18972aba4af440322fcd8d6c2..0000000000000000000000000000000000000000 --- a/packages/docs/opendesign.md +++ /dev/null @@ -1,66 +0,0 @@ -# opendesign 组件设计 - -## 需求背景 - -为了支撑 openEuler 社区前端体验提升,帮助更多开发者、Sig 组快速搭建自己的前端页面,故设计开发了一套前端组件库。 -该组件库需要支持: - -1. 强大的皮肤定制能力,支持开发者快速定制皮肤,实现页面风格一致; -2. 上手难度低,开箱即用,提供示例模版,能快速搭建项目页面; - -## 方案设计 - -1. 基于当前开发者能力现状(国内 vue 开发者最多),采用 vue3 作为组件开发框架,组件实现采用逻辑与 UI 分离; -2. 基于 Css Variables,结合 opendesign 设计体系,设计变量体系,定义全局变量以及每个组件变量,实现全局+局部皮肤定制能力; -3. 针对尺寸、颜色、圆角、图标等设置,提供全局配置接口,基于 vue 响应式机制,实现配置动态化 -4. 组件避免依赖 dom,支持服务端渲染; - -## 方案实现 - -### 变量体系 Token - -#### 1. 全局变量 - -- 色盘 Palette -- 颜色 Color - - 基础色 - - 状态色 - - 控件色 - - 填充色 - - 信息色 -- 阴影 Shadow -- 字体 Font -- 尺寸 Size - - 组件尺寸 - - 图标尺寸 - - 圆角尺寸 -- 间距 Gap -- 动画 Animation - - 动画曲线 - - 持续时间 - -> 例: [light.token.css](../opendesign/src/_styles/light.token.css) - -#### 2. 组件变量 - -基于每个组件状态、类型,定义组件变量,值引用全局变量. - -> 例: [button/var.css](../opendesign/src/button/style/var.scss) - -### 动态全局配置 - -1. 设置全局默认值,同时暴露接口进行默认值修改 -2. 组件内部图标统一配置,并支持接口修改,实现组件深度定制 - -#### 1. 配置全局样式 - -| 方法名 | 参数 | 返回值 | 说明 | -| :------------- | :-------------------------------------- | :----- | :----------------------- | -| initSize | (type: 'large' \| 'medium' \| 'small') | -- | 设置全局组件尺寸 | -| initRound | (type: 'pill' \| '') | -- | 设置全局组件圆角 | -| initZIndex | (val: number) | -- | 设置全局组件初始 z-index | -| initMediaPoint | point: Record<'phone' \| 'pad', number> | -- | 设置全局组件响应式断点 | - -#### 2. 配置全局图标 - -具体接口见[`init-icons.ts`](../opendesign/src/_utils/init-icons.ts) diff --git a/packages/docs/package.json b/packages/docs/package.json new file mode 100644 index 0000000000000000000000000000000000000000..0d2135950e137da91961862fdd7ed181a6a2e982 --- /dev/null +++ b/packages/docs/package.json @@ -0,0 +1,46 @@ +{ + "name": "docs", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "gen:icon": "open-scripts gen:icon --config ./icons/icon.config.ts", + "build": "vite build", + "type-check": "vue-tsc", + "preview": "vite preview", + "gen:api": "vite-node ./scripts/generateApi.ts" + }, + "dependencies": { + "@opensig/open-analytics": "catalog:utils", + "@opensig/open-scripts": "workspace:^", + "@shikijs/langs": "catalog:markdown", + "@shikijs/themes": "catalog:markdown", + "shiki": "catalog:markdown", + "@vue/compiler-dom": "catalog:vue", + "markdown-it": "catalog:markdown", + "pinia": "catalog:vue", + "prettier": "catalog:lint", + "ts-morph": "catalog:typescript", + "vue": "catalog:vue" + }, + "devDependencies": { + "@types/fs-extra": "catalog:typescript", + "@types/markdown-it": "catalog:typescript", + "@vue/compiler-sfc": "catalog:vue", + "fs-extra": "catalog:io", + "glob": "catalog:io", + "gray-matter": "catalog:markdown", + "magic-string": "catalog:utils", + "markdown-it-async": "catalog:markdown", + "sass": "catalog:css", + "typescript": "catalog:typescript", + "unplugin-vue-markdown": "catalog:markdown", + "vite": "catalog:build", + "vite-plugin-inspect": "catalog:build", + "vue-component-meta": "catalog:vue", + "vue-docgen-api": "catalog:vue", + "vue-tsc": "catalog:build", + "@opensig/open-scripts": "workspace:^" + } +} diff --git a/packages/docs/plugins/generateComponentRouter.ts b/packages/docs/plugins/generateComponentRouter.ts new file mode 100644 index 0000000000000000000000000000000000000000..244e7f892487628e9654029334ab74d19ce6c263 --- /dev/null +++ b/packages/docs/plugins/generateComponentRouter.ts @@ -0,0 +1,116 @@ +import { createFilter, type Plugin } from 'vite'; +import { glob } from 'glob'; +import { resolve } from 'path'; +import { fileURLToPath } from 'url'; +import fse from 'fs-extra'; +import matter from 'gray-matter'; +import * as prettier from 'prettier'; +import tsPlugin from 'prettier/plugins/typescript'; +import { getLangByFileName } from '../helper/utils'; + +const __fileName = fileURLToPath(import.meta.url); +const searchBase = resolve(__fileName, '../../../opendesign/src'); +const output = resolve(__fileName, '../../src/router/components.ts'); +let preRouteContent = ''; +function debounce) => any>(fn: T, wait: number = 0, runFirst: boolean = true) { + let handler = null; + return (...args: Array) => { + if (runFirst) { + if (!handler) { + fn(...args); + } + } + clearTimeout(handler); + handler = setTimeout(() => { + if (!runFirst) { + fn(...args); + } + handler = 0; + }, wait); + }; +} + +const emit = debounce(() => { + /** + * 检测 /packages/opendesing/OXxx/__docs__/index..md 中的文件,生成 /src/router/components.ts 路由文件 + */ + glob('**/__docs__/index.*.md', { cwd: searchBase, posix: true }) + .then((files) => { + return Promise.all( + files.sort().map(async (file) => { + const fullPath = resolve(searchBase, file); + const content = await fse.readFile(fullPath, 'utf-8'); + return { content, file, fullPath, name: file.match(/([^/]+)\/__docs__\/?/)?.[1], lang: getLangByFileName(file).lang }; + }), + ); + }) + .then((fileContents) => { + /** + * 解析markdown中的matter语法,将数据存储到route.meta中 + */ + const headCommentRegex = /^---\s*([\s\S]*?)\s*---/; + return fileContents.map((info) => { + const match = info.content.match(headCommentRegex); + const matterData = match ? matter(match[0]) : { data: {} }; + return { + ...info, + meta: { + ...matterData.data, + lang: info.lang, + sidebarName: 'components', + }, + }; + }); + }) + .then((res) => { + return `import { type RouteRecordRaw } from 'vue-router'; +export const routes: Array = [ +${res + .map( + (info) => ` { + path: '/${info.lang}/components/${info.name}', + name: 'component/${info.name}/${info.lang}', + component: () => import('@opensig/opendesign/${info.file}'), + meta: ${JSON.stringify(info.meta)} + }`, + ) + .join(',')} +]; + `; + }) + .then((res) => { + if (res === preRouteContent) { + // 避免不必要的更新导致页面自动刷新 + return; + } + preRouteContent = res; + // 使用prettier格式化输出的代码 + return prettier.format(res, { parser: 'typescript', plugins: [tsPlugin], singleQuote: true, printWidth: 160 }); + }) + .then((res) => { + if (res) { + // 写代码文件 + return fse.writeFile(output, res); + } + }); +}, 1000); + +export default function generateComponentRouter(): Plugin { + const filter = createFilter(/opendesign\/src\/.*?\/__docs__\/index\..*?\.md$/); + return { + name: 'generate-component-router', + configureServer(server) { + // 监听searchBase文件夹 + server.watcher.add(searchBase); + server.watcher.on('all', (event, path) => { + // 当有/packages/opendesing/OXxx/__docs__/index..md 文件增删改时重新生成 router/components.ts 文件 + if (filter(path.replace(/\\/g, '/')) && ['add', 'unlink', 'change'].includes(event)) { + emit(); + } + }); + }, + buildStart() { + emit(); + }, + }; +} diff --git a/packages/docs/plugins/injectDemoAndApi.ts b/packages/docs/plugins/injectDemoAndApi.ts new file mode 100644 index 0000000000000000000000000000000000000000..d808b50587ab44b715da9b11f364427d70c467cc --- /dev/null +++ b/packages/docs/plugins/injectDemoAndApi.ts @@ -0,0 +1,205 @@ +import { type Plugin, type ViteDevServer } from 'vite'; +import { join, dirname } from 'node:path'; +import { promises as fsp } from 'node:fs'; +import { getLangByFileName } from '../helper/utils'; +import { parse } from '@vue/compiler-sfc'; +import { parseDocsCode, generateCode, asyncReplace } from '../helper/utils'; + +const entryFileRegex = /index\.([\w-]+)\.md$/; +const virtualModules = new Map(); + +type ImportMeta = { path: string; default?: string; all?: string; lang?: string }; +type ImportRecord = Record; +/** 生成 import 语句 */ +const genImportedExpression = (imported: ImportRecord) => { + return Object.values(imported) + .map((item) => { + let importStr = 'import '; + if (item.default) { + importStr += `${item.default} `; + } else if (item.all) { + importStr += `* as ${item.all} `; + } + importStr += `from ${JSON.stringify(item.path)};`; + return importStr; + }) + .join('\n'); +}; +const resolveActiveTheme = (theme: string) => { + let _theme = theme.toLowerCase(); + if (_theme.startsWith(':')) { + _theme = _theme.slice(1); + } + return Array.from( + new Set( + _theme + .split('|') + .map((_item) => { + const item = _item.trim().toLowerCase(); + if (['e', 'open-euler'].includes(item)) { + return 'e'; + } + if (['k', 'kunpeng'].includes(item)) { + return 'k'; + } + if (['a', 'ascend'].includes(item)) { + return 'a'; + } + if (['d', 'open-design'].includes(item)) { + return 'd'; + } + return ''; + }) + .filter(Boolean), + ), + ); +}; + +/** + * 转化 case usage 指令导入的组件 + * @param code 组件源代码 + * @param id 入口文件id + * @param mode 插件运行环境 + * @param activeTheme 激活组件的主题,分为 e, a, k,分别代表在仅在 Euler、Kunpeng、Ascend 主题下可见 + * @param viteDevServer vite开发服务器实例 + * @returns 转化后的代码 + */ +const transformVueDemo = (code: string, id: string, mode: 'dev' | 'build' | 'unknown', activeThemes?: string[], viteDevServer?: ViteDevServer) => { + const imported: ImportRecord = {}; + const { customBlocks: _customBlocks, scriptSetup: _scriptSetup, styles } = parse(code).descriptor; + // 因为要修改 customBlocks 和 scriptSetup,所以复制一份 + const customBlocks = [..._customBlocks]; + const scriptSetup = _scriptSetup ? { ..._scriptSetup } : null; + // 处理 docs 自定义块 + const docsBlockIdx = customBlocks.findIndex((block) => { + if (block.type === 'docs' && block.lang === 'md') { + return true; + } + }); + if (docsBlockIdx >= 0) { + // 将 docs 自定义块按 zh-CN,en-US 分隔为多个虚拟模块然后导入。这些虚拟模块后经由 vueMdTranslate 插件编译为 vue 组件 + parseDocsCode(customBlocks[docsBlockIdx].content).forEach(({ lang: docsLang, code: docCode }, index) => { + const virtualId = join(dirname(id), `virtual-${docsLang}.md`).replace(/\\/g, '/'); + const defaultName = `AutoInjectDocs${index}`; + imported[defaultName] = { path: virtualId, default: `AutoInjectDocs${index}`, lang: docsLang }; + const preDocCode = virtualModules.get(virtualId); + if (mode === 'dev' && viteDevServer && preDocCode && preDocCode !== docCode) { + // 通知虚拟文件更新 + viteDevServer.watcher.emit('change', virtualId); + } + virtualModules.set(virtualId, docCode); + }); + customBlocks.splice(docsBlockIdx, 1); + if (scriptSetup) { + // 导入 docs 虚拟模块 + scriptSetup.content = `${scriptSetup.content}\n;${genImportedExpression(imported)}`; + } + } + if (scriptSetup) { + let styleCode = ''; + styles.forEach((styleItem) => { + styleCode += `${generateCode(styleItem)}`; + }); + scriptSetup.content = `${scriptSetup.content}\n;const _style = ${JSON.stringify(styleCode)};\n`; + } + const docsJson = `{${Object.values(imported) + .map((item) => `'${item.lang}': ${item.default}`) + .join(',')}}`; + // 补充 usage 的 vue 文件的 template 块 + const template = ``; + return `${[...customBlocks, scriptSetup, ...styles] + .filter(Boolean) + .map((block) => { + return generateCode(block); + }) + .join('\n')}\n${template}`; +}; + +/** 将指令case usage api转化为文件导入 */ +const transformMdEntry = async (code: string, id: string, usageFiles: Map) => { + const imported: ImportRecord = {}; + const lang = getLangByFileName(id); + // 将 注释替换成 + // 将 注释替换成 + let newCode = await asyncReplace(code, //gi, async (match) => { + const [, directive, _activeTheme, filePath] = match; + const paths = filePath.split('/'); + const dirs = paths.slice(0, -1); + const fileName = paths[paths.length - 1]; + const activeThemes = resolveActiveTheme(_activeTheme || ''); + if (directive === 'api') { + // 导入 api 文件 + // 通过导入而非字符串替换的原因是为了将 api 文件作为一个独立的模块, + // 当 api 文件自身更新时才能独立触发热更新 + const apiFile = join(dirname(id), ...dirs, `${fileName}-api.${lang.lang}.md`); + const defaultName = `AutoInjectApi${fileName}`; + if (await fsp.stat(apiFile).catch(() => false)) { + imported[defaultName] = { path: apiFile, default: defaultName }; + return `<${defaultName} />`; + } + } + // 导入 demo 文件 + const demoFile = join(dirname(id), ...dirs, `./__case__/${fileName}.vue`); + if (await fsp.stat(demoFile).catch(() => false)) { + const importedName = `AutoInject${fileName}`; + if (!imported[importedName]) { + imported[importedName] = { + path: demoFile, + default: importedName, + }; + } + if (directive === 'case') { + return ``; + } + const usageFileId = demoFile.replace(/\\/g, '/'); + usageFiles.set(usageFileId, activeThemes); + return `<${importedName} />`; + } + return match[0]; + }); + // 插入需要导入的模块 + const importExp = genImportedExpression(imported); + if (importExp) { + newCode += `\n\n\n`; + } + return newCode; +}; + +/** + * vite 插件,在 index..md 添加 /__docs__/__case__ 组件;拼接 api.zh-CN.md 文件 + * @returns Plugin + */ +export function injectDemoAndApi(): Plugin { + /** + * 缓存需要导入的usage模块 + * Map + */ + const usageFiles = new Map(); + let viteDevServer: ViteDevServer | null = null; + return { + name: 'portal:inject-demo-and-api', + enforce: 'pre', + configureServer(server) { + viteDevServer = server; + }, + resolveId(id) { + if (virtualModules.has(id)) { + return id; + } + }, + load(id) { + return virtualModules.get(id); + }, + transform(code, id) { + // 处理 vue 文件 + if (usageFiles.has(id)) { + return transformVueDemo(code, id, this.environment.mode, usageFiles.get(id), viteDevServer); + } + + // 处理 md 入口文件 + if (entryFileRegex.test(id) && !id.startsWith('virtual:')) { + return transformMdEntry(code, id, usageFiles); + } + }, + }; +} diff --git a/packages/docs/plugins/injectDemoDocs.ts b/packages/docs/plugins/injectDemoDocs.ts new file mode 100644 index 0000000000000000000000000000000000000000..3a3827c0bac5c4b793031e4b000fbc6fc8514779 --- /dev/null +++ b/packages/docs/plugins/injectDemoDocs.ts @@ -0,0 +1,76 @@ +import { type Plugin, type ViteDevServer } from 'vite'; +import { parseDocsCode } from '../helper/utils'; + +const parseVueQuery = (id: string) => { + const [file, _query] = id.split('?', 2); + if (!_query) { + return { file, query: {}, queryExtension: '' }; + } + const queryExtension = _query.match(/\.([a-zA-Z0-9]+)$/)?.[1]; + const query = queryExtension ? _query.slice(0, _query.length - queryExtension.length - 1) : _query; + const queryObj = query + ? query.split('&').reduce( + (prev, curr) => { + const [key, value] = curr.split('='); + prev[key] = value || true; + return prev; + }, + {} as Record, + ) + : {}; + return { file, query: queryObj, queryExtension }; +}; + +const virtualModules = new Map(); +const genVirtualId = (id: string, lang: string) => { + return `${id.split('?')[0]}-virtual-${lang}.md`; +}; +/** + * vite插件,将vue文件中的自定义块 docs 中的 markdown 保存到_sfc_main.__docs中, + * 该内容会作为对case组件的富文本描述,被DemoContainer组件使用 + * @returns Plugin + */ +export function injectDemoDocs(): Plugin { + let viteDevServer: ViteDevServer | null = null; + return { + name: 'portal:inject-demo-docs', + configureServer(server) { + viteDevServer = server; + }, + resolveId(id) { + if (virtualModules.has(id)) { + return id; + } + }, + load(id) { + return virtualModules.get(id)?.langCode; + }, + transform(code, id) { + const { query } = parseVueQuery(id); + if (!query.vue || query.type !== 'docs' || !id.endsWith('.md')) { + return; + } + const virtualIds: string[] = []; + parseDocsCode(code).forEach(({ lang, code: langCode }) => { + const virtualId = genVirtualId(id, lang); + if (virtualModules.has(virtualId) && this.environment.mode === 'dev' && viteDevServer) { + // 刷新虚拟模块,通知浏览器更新 + viteDevServer.watcher.emit('change', virtualId); + } + virtualModules.set(virtualId, { langCode, lang }); + virtualIds.push(virtualId); + }); + return `${virtualIds.map((vid, index) => `import Docs${index} from ${JSON.stringify(vid)};`).join('\n')} +export default function (_sfc_main) { + _sfc_main.__docs = { +${virtualIds + .map((vid, index) => { + const { lang } = virtualModules.get(vid)!; + return ` '${lang}': Docs${index}`; + }) + .join(',\n')} + }; +}`; + }, + }; +} diff --git a/packages/docs/plugins/injectDemoSource.ts b/packages/docs/plugins/injectDemoSource.ts new file mode 100644 index 0000000000000000000000000000000000000000..9c87029ae9ba4ce5ede742430be582d95ef45a02 --- /dev/null +++ b/packages/docs/plugins/injectDemoSource.ts @@ -0,0 +1,86 @@ +import fsp from 'node:fs/promises'; +import { createFilter, type Plugin } from 'vite'; +import { parse, type SFCDescriptor } from '@vue/compiler-sfc'; +import { generateCode } from '../helper/utils'; + +const virtualModules = new Map(); +const getVirtualId = (id: string) => { + return `${id}-demo-source.md`; +}; +/** + * 使用@vue/compiler-sfc库,只保留case源代码中的 script, scriptSetup, template, styles 块, + * 并将清理后的源代码渲染为 vue 组件 + * @param source case文件源代码 + * @returns 经过清理后的源代码组件 + */ +const generateVirtualModule = (descriptor: SFCDescriptor) => { + let cleanedSource = ''; + + if (descriptor.script) { + cleanedSource = generateCode(descriptor.script); + } + if (descriptor.scriptSetup) { + cleanedSource += generateCode(descriptor.scriptSetup); + } + if (descriptor.template) { + cleanedSource += generateCode(descriptor.template); + } + if (descriptor.styles) { + descriptor.styles.forEach((style) => { + cleanedSource += generateCode(style); + }); + } + cleanedSource = cleanedSource.trimEnd(); + // 返回组件源码 + return `\`\`\`vue:line-numbers\n${cleanedSource}\n\`\`\``; +}; +/** + * vite 插件,用于将 Case 组件的源代码保存到 _sfc_main 对象中 + * @returns Plugin + */ +export function injectDemoSource(): Plugin { + const filter = createFilter(/opendesign\/src\/.*?\/__case__\/.+\.vue$/); + return { + name: 'portal:inject-demo-source', + resolveId(id) { + if (virtualModules.has(id)) { + return id; + } + }, + load(id) { + // 返回虚拟模块 + return virtualModules.get(id); + }, + async transform(code, id) { + if (!filter(id)) { + return; + } + if (await fsp.stat(id).then((stat) => stat.isFile())) { + const source = await fsp.readFile(id, 'utf-8'); + const virtualId = getVirtualId(id); + const { descriptor } = parse(source); + if (!descriptor.template) { + // 无 template 块,属于 Usage 运行时编译组件,不需要生成 DemoSource + return; + } + virtualModules.set(virtualId, generateVirtualModule(descriptor)); + // Case 组件引入虚拟模块 virtualId,该虚拟模块就是 Case 组件的源代码 + return `${code} +;import _DemoSource from ${JSON.stringify(virtualId)}; +_sfc_main.DemoSource = _DemoSource;`; + } + }, + async handleHotUpdate(ctx) { + const virtualId = getVirtualId(ctx.file); + if (virtualModules.has(virtualId)) { + // 当Case组件更新时,同时更新对应的虚拟模块,以实现源码的热更新 + const { descriptor } = parse(await ctx.read()); + if (!descriptor.template) { + return; + } + virtualModules.set(virtualId, generateVirtualModule(descriptor)); + ctx.server.watcher.emit('change', virtualId); + } + }, + }; +} diff --git a/packages/docs/plugins/markdown/common.ts b/packages/docs/plugins/markdown/common.ts new file mode 100644 index 0000000000000000000000000000000000000000..7bc256567a680c79b6f2b52ad09c3da8a1ca4658 --- /dev/null +++ b/packages/docs/plugins/markdown/common.ts @@ -0,0 +1,13 @@ +import { type MarkdownItAsyncOptions } from 'markdown-it-async'; +import lineNumber from './lineNumber'; +import popover from './popover'; +import wrapTable from './wrapTable'; +import wrapCodeContainer from './wrapCodeContainer'; +import link from './link'; + +export const markdownItOptions: MarkdownItAsyncOptions = { + html: true, + linkify: true, + typographer: true, +}; +export const markdownItPlugins = [lineNumber, popover, wrapCodeContainer, wrapTable, link]; diff --git a/packages/docs/plugins/markdown/highlight.ts b/packages/docs/plugins/markdown/highlight.ts new file mode 100644 index 0000000000000000000000000000000000000000..cd2624bbbd5a222f4f6a286f5588d4c3f75786f8 --- /dev/null +++ b/packages/docs/plugins/markdown/highlight.ts @@ -0,0 +1,105 @@ +import { escapeHtml } from 'markdown-it/lib/common/utils.mjs'; +import { createHighlighterCore, createJavaScriptRegexEngine } from 'shiki'; +import { generateCode } from '../../helper/utils'; +// 由于 @vue/compiler-sfc 包体积过大,因此使用简化的 parseSfc 函数 +import { parseSfc } from '../../helper/vue-utils'; + +const baseConfig = { + themes: [import('@shikijs/themes/light-plus'), import('@shikijs/themes/dark-plus')], + engine: createJavaScriptRegexEngine(), +}; +/** + * 创建高亮函数 + * @returns 高亮函数 + */ +export const createHighlighter = async () => { + const [mainHighlighter, vueTemplateHighlighter] = await Promise.all([ + createHighlighterCore({ + ...baseConfig, + langs: [ + import('@shikijs/langs/js'), + import('@shikijs/langs/ts'), + import('@shikijs/langs/json'), + import('@shikijs/langs/html'), + import('@shikijs/langs/css'), + import('@shikijs/langs/bash'), + import('@shikijs/langs/shell'), + import('@shikijs/langs/vue'), + import('@shikijs/langs/md'), + import('@shikijs/langs/yaml'), + import('@shikijs/langs/jsx'), + import('@shikijs/langs/tsx'), + import('@shikijs/langs/scss'), + import('@shikijs/langs/less'), + ], + }), + // 创建Vue模板专用高亮器 + // 实测发现 markdown, yaml, jsx, tsx, scss, less 语言包会影响 vue 的 template 块高亮,导致 vue 的特殊语法高亮不准确,因此创建一个专用的模板高亮器 + createHighlighterCore({ + ...baseConfig, + langs: [ + import('@shikijs/langs/vue'), + import('@shikijs/langs/js'), + import('@shikijs/langs/ts'), + import('@shikijs/langs/html'), + import('@shikijs/langs/css'), + ], + }), + ]); + + const stripPreCodeReg = /([\s\S]*?)<\/code><\/pre>/; + /** 去除高亮代码的首尾
...
*/ + const stripPreCodeTag = (htmlCode: string) => htmlCode.replace(stripPreCodeReg, '$1'); + + // 支持语言集合 + const supportLangs = new Set([ + 'javascript', + 'js', + 'typescript', + 'ts', + 'json', + 'html', + 'css', + 'bash', + 'shell', + 'vue', + 'markdown', + 'md', + 'yaml', + 'jsx', + 'tsx', + 'scss', + 'less', + ]); + + const themeConfig = { + themes: { light: 'light-plus', dark: 'dark-plus' }, + defaultColor: false as const, + }; + + return function highlight(code: string, lang: string) { + if (!supportLangs.has(lang)) { + return escapeHtml(code) + .split('\n') + .map((line) => `${line}`) + .join('\n'); + } + + if (lang === 'vue') { + const descriptor = parseSfc(code); + // vue的template模块需要单独处理,因此分块高亮 + const blocks = [descriptor.script, ...descriptor.styles, descriptor.scriptSetup, ...descriptor.customBlocks, descriptor.template] + .filter(Boolean) + .sort((a, b) => a.loc.start.offset - b.loc.start.offset); + + return blocks + .map((block) => { + const highlighter = block.type === 'template' ? vueTemplateHighlighter : mainHighlighter; + return stripPreCodeTag(highlighter.codeToHtml(generateCode(block), { ...themeConfig, lang: 'vue' })); + }) + .join('\n'); + } + + return stripPreCodeTag(mainHighlighter.codeToHtml(code, { ...themeConfig, lang })); + }; +}; diff --git a/packages/docs/plugins/markdown/lineNumber.ts b/packages/docs/plugins/markdown/lineNumber.ts new file mode 100644 index 0000000000000000000000000000000000000000..365f1330a8161f9f30910b8cb3a1c28155e2fd0b --- /dev/null +++ b/packages/docs/plugins/markdown/lineNumber.ts @@ -0,0 +1,39 @@ +import { type MarkdownItAsync } from 'markdown-it-async'; + +export const LINENUMBER_TAG_ATTR = 'data-linenumber-start'; +export const LINENUMBER_CSS_ATTR = '--linenumber-start'; +/** + * markdown 插件,给pre添加 data-linenumber-start 以便属性选择器添加行号样式,给code添加css变量 --linenumber-start,以控制行号开始数字 + * @param md markdown-it 实例 + */ +export default function lineNumber(md: MarkdownItAsync) { + const fence = md.renderer.rules.fence; + const preCodeReg = /([\s\S]*?<\/code><\/pre>)/; + md.renderer.rules.fence = (tokens, idx, options, env, self) => { + // 当代码块中有行号指令时添加行号,指令的格式为 :line-numbers[=start] + // 如:```vue:line-numbers=3 表示显示行号,且行号从3开始 + const lineNumberReg = /:line-numbers(=\d+)?/; + const token = tokens[idx]; + const lineNumberMatch = token.info.match(lineNumberReg); + let originalCode = ''; + if (lineNumberMatch) { + let start = parseInt(lineNumberMatch[1]?.slice(1) || '1'); + start = Number.isNaN(start) ? 1 : start; + token.info = tokens[idx].info.replace(lineNumberMatch[0], ''); + let cssStr = token.attrGet('style') || ''; + if (cssStr && !cssStr.trimEnd().endsWith(';')) { + cssStr += ';'; + } + cssStr += `${LINENUMBER_CSS_ATTR}: ${start};`; + token.attrSet('style', cssStr); + originalCode = fence(tokens, idx, options, env, self); + + const codeMatch = originalCode.match(preCodeReg); + if (codeMatch) { + const [, preAttr, rest] = codeMatch; + return `${rest}`; + } + } + return fence(tokens, idx, options, env, self); + }; +} diff --git a/packages/docs/plugins/markdown/link.ts b/packages/docs/plugins/markdown/link.ts new file mode 100644 index 0000000000000000000000000000000000000000..5bf27d722a302bed4259203fbd30755ecb3865e1 --- /dev/null +++ b/packages/docs/plugins/markdown/link.ts @@ -0,0 +1,8 @@ +import { MarkdownItAsync } from 'markdown-it-async'; +export default function customLinkPlugin(md: MarkdownItAsync) { + md.renderer.rules.link_open = (tokens, idx) => { + const token = tokens[idx]; + return ` `${key}="${value}"`).join(' ')}>`; + }; + md.renderer.rules.link_close = () => ''; +} diff --git a/packages/docs/plugins/markdown/popover.ts b/packages/docs/plugins/markdown/popover.ts new file mode 100644 index 0000000000000000000000000000000000000000..cd81df43b449aedf90f3a620ea253ee7209defc0 --- /dev/null +++ b/packages/docs/plugins/markdown/popover.ts @@ -0,0 +1,57 @@ +import { MarkdownItAsync } from 'markdown-it-async'; + +const HTML_REPLACEMENTS = { + // 避免xss注入 + '<': '<', + '>': '>', + '"': '"', + "'": ''', +}; +function replaceCellChar(ch: string) { + return HTML_REPLACEMENTS[ch]; +} +function escapeHtml(value?: string) { + const HTML_ESCAPE_REPLACE_RE = /[<>"']/g; + return value ? value.replace(HTML_ESCAPE_REPLACE_RE, replaceCellChar) : ''; +} +/** + * markdown插件,通过 ^[标题]`内容` 语法,渲染popover,popover组件的target为 标题 + * @param md markdown-it 实例 + */ +export default function popover(md: MarkdownItAsync) { + // 定义正则表达式,匹配 ^[内容]`提示信息` 语法 + const popoverRegExp = /^\^\[([^\]]*)\](?:\((normal|primary|success|warning|danger)\))?(`[^`]*`)?/; + + md.renderer.rules.popover = function (tokens, idx) { + const token = tokens[idx]; + const content = escapeHtml(token.content); + const info = escapeHtml(token.info); + const tagCode = `${content}`; + return info + ? ` + +${info} +` + : tagCode; + }; + + md.inline.ruler.before('emphasis', 'popover', function (state, silent) { + const code = state.src.slice(state.pos, state.posMax); + const matched = code.match(popoverRegExp); + if (!matched) { + return false; + } + if (!silent) { + const token = state.push('popover', 'popover', 0); + token.content = matched[1].replace(/\\\|/g, '|'); + if (!token.meta) { + token.meta = {}; + } + token.meta.color = matched[2] || 'normal'; + token.info = (matched[3] || '').replace(/^`(.*)`$/, '$1'); + token.level = state.level; + state.pos += matched[0].length; + } + return true; + }); +} diff --git a/packages/docs/plugins/markdown/vueMdTranslate.ts b/packages/docs/plugins/markdown/vueMdTranslate.ts new file mode 100644 index 0000000000000000000000000000000000000000..376905b2a4ea1a67a0343634055c83e1f0a0e870 --- /dev/null +++ b/packages/docs/plugins/markdown/vueMdTranslate.ts @@ -0,0 +1,17 @@ +import Markdown from 'unplugin-vue-markdown/vite'; +import { MarkdownItAsync } from 'markdown-it-async'; +import { markdownItOptions, markdownItPlugins } from './common'; +import { createHighlighter } from './highlight'; + +export const markdownItSetup = async function (md: MarkdownItAsync) { + markdownItPlugins.forEach((item) => md.use(item)); + md.options.highlight = await createHighlighter(); +}; +/** + * vite 插件,markdown中可导入并使用vue组件;同时将 markdown 转换为 vue 组件 + */ +export const plugin = Markdown({ + markdownItOptions, + markdownItSetup, + exclude: /\?vue&type=docs/, +}); diff --git a/packages/docs/plugins/markdown/wrapCodeContainer.ts b/packages/docs/plugins/markdown/wrapCodeContainer.ts new file mode 100644 index 0000000000000000000000000000000000000000..f5b805cff05cd891bc02527615030f8a032c77a0 --- /dev/null +++ b/packages/docs/plugins/markdown/wrapCodeContainer.ts @@ -0,0 +1,20 @@ +import { MarkdownItAsync } from 'markdown-it-async'; + +/** + * markdown插件,使用 CodeContainer 组件包裹代码块,实现代码块的复制功能,显示代码块语言功能以及横向滚动功能 + * @param md markdown-it 实例 + */ +export default function wrapCodeContainer(md: MarkdownItAsync) { + const fence = md.renderer.rules.fence; + md.renderer.rules.fence = (tokens, idx, options, env, self) => { + const token = tokens[idx]; + if (!token.attrGet('v-pre')) { + token.attrSet('v-pre', ''); + } + const preCode = fence(tokens, idx, options, env, self); + return ` + + ${preCode} +`; + }; +} diff --git a/packages/docs/plugins/markdown/wrapTable.ts b/packages/docs/plugins/markdown/wrapTable.ts new file mode 100644 index 0000000000000000000000000000000000000000..91bb656eb880132a4d1d52866c2ef43b7208829e --- /dev/null +++ b/packages/docs/plugins/markdown/wrapTable.ts @@ -0,0 +1,15 @@ +import { type MarkdownItAsync } from 'markdown-it-async'; + +/** + * markdown插件,给表格添加符合社区规范的样式 + * @param md markdown-it instance + */ +export default function wrapTable(md: MarkdownItAsync) { + md.renderer.rules.table_open = function (tokens, idx, options, env, self) { + const attrs = tokens[idx].attrs || self.renderAttrs(tokens[idx]); + return `
`; + }; + md.renderer.rules.table_close = function () { + return '
'; + }; +} diff --git a/packages/docs/public/avatar.svg b/packages/docs/public/avatar.svg new file mode 100644 index 0000000000000000000000000000000000000000..995fccea84ef47fa3aea61281253f4e4145352a0 --- /dev/null +++ b/packages/docs/public/avatar.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/docs/public/card-cover.jpg b/packages/docs/public/card-cover.jpg new file mode 100644 index 0000000000000000000000000000000000000000..8bdf357265a5a4a42561ea2528b44278c195951a Binary files /dev/null and b/packages/docs/public/card-cover.jpg differ diff --git a/packages/docs/public/favicon.ico b/packages/docs/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..683c4317d540c87c4ad36b559e05814d410292a1 Binary files /dev/null and b/packages/docs/public/favicon.ico differ diff --git a/packages/docs/public/opendesign-logo-dark.png b/packages/docs/public/opendesign-logo-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..9b90c7b1bfc792b20397106b0c720e1ae507c540 Binary files /dev/null and b/packages/docs/public/opendesign-logo-dark.png differ diff --git a/packages/docs/public/opendesign-logo-light.png b/packages/docs/public/opendesign-logo-light.png new file mode 100644 index 0000000000000000000000000000000000000000..24633e626b911f5d9c172674a5783d18ecb9a2ec Binary files /dev/null and b/packages/docs/public/opendesign-logo-light.png differ diff --git a/packages/docs/radio.md b/packages/docs/radio.md deleted file mode 100644 index dbb47a1727603ade19470e3a49eefba859316f8e..0000000000000000000000000000000000000000 --- a/packages/docs/radio.md +++ /dev/null @@ -1,52 +0,0 @@ -# Radio 单选框 - -## props - -| name | type | 默认值 | 说明 | -| :------------------ | :-------------------------- | :----- | -------------------------------- | -| value | string \| number \| boolean | - | 必选,单选框 value | -| modelValue(v-model) | string \| number \| boolean | - | 可选,单选框双向绑定值 | -| defaultChecked | boolean | false | 可选,非受控状态时,默认是否选中 | -| disabled | boolean | false | 可选,是否禁用 | - -## event - -| name | 参数 | 说明 | -| :----- | :------------------------------- | :----------------------------------- | -| change | val: string \| number \| boolean | 双向绑定值改变时,在选中单选框上触发 | - -## expose - -| name | type | 说明 | -| :------ | :------ | :------------- | -| checked | boolean | 单选框是否选中 | - -## slot - -| name | 参数 | 说明 | -| :------ | :--------------------------------- | :------------- | -| radio | checked:boolean; disabled: boolean | 自定义单选框 | -| default | | 单选框文字内容 | - -# RadioGroup 单选框组 - -## props - -| name | type | 默认值 | 说明 | -| :------------------ | :-------------------------- | :----- | ---------------------------------- | -| modelValue(v-model) | string \| number \| boolean | - | 可选,单选框组双向绑定值 | -| defaultValue | string \| number \| boolean | '' | 可选,非受控状态时,单选框组默认值 | -| disabled | boolean | false | 可选,是否禁用 | -| direction | 'h' \| 'v' | 'h' | 可选,排列方向 | - -## event - -| name | 参数 | 说明 | -| :----- | :------------------------------- | :------------- | -| change | val: string \| number \| boolean | 状态切换后触发 | - -## slot - -| name | 参数 | 说明 | -| :------ | :--- | :----------- | -| default | | 单选框组内容 | diff --git a/packages/docs/rate.md b/packages/docs/rate.md deleted file mode 100644 index 02e258ba0a795573d07bcdf52cb453bce0242c09..0000000000000000000000000000000000000000 --- a/packages/docs/rate.md +++ /dev/null @@ -1,27 +0,0 @@ -# Rate 评分 - -## props - -| name | type | 默认值 | 说明 | -| :------------------ | :--------------------------------------------- | :------- | --------------------------------------------------- | -| count | number | 5 | 可选,评分数量 | -| modelValue(v-model) | number | - | 可选,双向绑定值 | -| defaultValue | number | 0 | 可选,非受控状态时,默认值 | -| size | 'large' \| 'normal' \| 'small' | 'normal' | 评分尺寸 | -| color | 'normal' \| 'primary' \| 'warning' \| 'danger' | 'normal' | 评分颜色 | -| readonly | boolean | false | 可选,是否只读 | -| allowHalf | boolean | false | 可选,是否支持半选 | -| clearable | boolean | false | 可选,是否支持可清空 | -| labels | Array | - | 提示文字,当且仅当提示文字数组长度等于 count 时生效 | - -## event - -| name | 参数 | 说明 | -| :----- | :------------ | :------------------- | -| change | index: number | 双向绑定值改变时触发 | - -## slot - -| name | 参数 | 说明 | -| :--- | :---------------------------- | :------------- | -| icon | index: number; status: string | 自定义评分图标 | diff --git a/packages/docs/result.md b/packages/docs/result.md deleted file mode 100644 index ff6727e9ec45055f6ade8f929da1fd334bbde0d9..0000000000000000000000000000000000000000 --- a/packages/docs/result.md +++ /dev/null @@ -1,19 +0,0 @@ -# result 结果页 - -## props - -| name | type | 默认值 | 说明 | -| :---------- | :------------------------------------------- | :----- | ---------------- | -| status | 'info' \| 'success' \| 'warning' \| 'danger' | - | 可选,结果页状态 | -| title | string | - | 可选,结果页标题 | -| description | string | - | 可选,结果页描述 | - -## slot - -| name | 参数 | 说明 | -| :---------- | :--- | :--------------- | -| icon | | 自定义图标 | -| title | | 自定义标题 | -| description | | 自定义描述 | -| extra | | 自定义操作区域 | -| default | | 自定义结果页内容 | diff --git a/packages/docs/scripts/generateApi.ts b/packages/docs/scripts/generateApi.ts new file mode 100644 index 0000000000000000000000000000000000000000..9876b6ac8d39afc5d06fd2207a8f3c74dfd1462d --- /dev/null +++ b/packages/docs/scripts/generateApi.ts @@ -0,0 +1,226 @@ +import { glob } from 'glob'; +import { fileURLToPath } from 'url'; +import fsp from 'node:fs/promises'; +import { join, dirname } from 'node:path'; +import { type ComponentMeta, createChecker } from 'vue-component-meta'; +import { parseMulti } from 'vue-docgen-api'; +import parseSlotsAndExpose from './parseSlotsAndExpose'; + +const __dirname = fileURLToPath(new URL('.', import.meta.url)); +const base = join(__dirname, '../../opendesign/'); +const srcDir = join(base, 'src'); +const tsConfigPath = join(base, 'tsconfig.app.json'); + +const checker = createChecker(tsConfigPath, { + forceUseTs: true, + noDeclarations: true, + printer: { newLine: 1 }, +}); +const CELL_REPLACEMENTS = { + // 避免xss注入 + '<': '<', + '>': '>', + '"': '"', + // unplugin-vue-markdown 插件不能正确处理单引号 + "'": ''', + // 竖线符号在markdown中会被解析为表格分隔符 + '|': '|', + // 表格中有换行符破坏markdown表格结构 + '\r': '', + '\n': '
', +}; +function replaceCellChar(ch: string) { + return CELL_REPLACEMENTS[ch]; +} +function escapeTableValue(value?: string) { + const CELL_ESCAPE_REPLACE_RE = /[<>"'|\r\n]/g; + return value ? value.replace(CELL_ESCAPE_REPLACE_RE, replaceCellChar) : ''; +} +function escapeInlineCode(value: string) { + return value.replace(/`/g, '\\`'); +} +function cleanTableData(table: any[][]) { + // 清理表格数据 + table.forEach((row) => { + row.forEach((cell, cellIdx) => { + row[cellIdx] = escapeTableValue(cell); + }); + }); + // 删除空列 + const columnCount = table[0].length; + const emptyIndexes = Array(columnCount).fill(true); + for (let i = 0; i < columnCount; i++) { + for (let j = 1; j < table.length; j++) { + if (table[j][i]) { + emptyIndexes[i] = false; + break; + } + } + } + return table.map((row) => row.filter((_, i) => !emptyIndexes[i])); +} +/** + * 将数组渲染为markdown表格 + * @param table 待处理的表格数据 + * @returns markdown表格 + */ +function markdownTable(table: string[][]) { + let code = ''; + // head + code += `| ${table[0].join(' | ')} |\n`; + code += `| ${table[0].map(() => '---').join(' | ')} |\n`; + // body + for (let i = 1; i < table.length; i++) { + code += `| ${table[i].join(' | ')} |\n`; + } + return code; +} +/** + * 通过vue-docgen-api库补充vue-component-meta库未能获取的Event描述 + * @param filename 待解析的vue文件 + * @param componentMeta + * @returns + */ +async function applyTempFixForEventDescriptions(filename: string, componentMeta: ComponentMeta) { + const hasEvents = componentMeta.events.length; + + if (!hasEvents) { + return; + } + + try { + const parsedComponentDocs = await parseMulti(filename, { modules: [srcDir], nameFilter: ['default'] }); + componentMeta.events = componentMeta.events.map((event) => { + const parsedEvent = parsedComponentDocs[0].events.find((item) => item.name === event.name); + + if (parsedEvent) { + event.description = parsedEvent.description; + } + + return event; + }); + } catch { + // noop + } +} +/** + * 补充 vue-component-meta 未能解析 defineSlots 的描述和签名 + * @param filePath 待解析的vue文件 + * @param componentMeta vue组件元数据 + * @returns 新的组件元数据 + */ +async function applyTempFixForSlotAndExpose(filePath: string, componentMeta: ComponentMeta) { + const { slots: slotMeta, exposes } = await parseSlotsAndExpose(filePath); + + slotMeta.forEach((slot) => { + const meta = componentMeta.slots.find((item) => item.name === slot.name); + if (meta) { + meta.description = slot.description; + meta.type = slot.type; + } else { + componentMeta.slots.push(slot); + } + }); + const exposed = exposes.map((expose) => componentMeta.exposed.find((item) => item.name === expose)); + componentMeta.exposed.length = 0; + componentMeta.exposed.push(...exposed); +} +const pathReg = /\/(O.*)\.vue/; +const tagTypes = { + deprecated: '(warning)', +}; +const exposeDesReg = /^\s*expose:([\s\S]+)/; +console.time('GenerateApi done'); +const promise = glob('*/O*.vue', { cwd: srcDir, posix: true }).then((files) => { + const promises = files.map(async (file) => { + const fullPath = join(srcDir, file); + // 解析Vue组件Api元数据 + const meta = checker.getComponentMeta(fullPath); + + await applyTempFixForEventDescriptions(fullPath, meta); + await applyTempFixForSlotAndExpose(fullPath, meta); + const pathMath = file.match(pathReg); + for (const lang of ['zh-CN', 'en-US']) { + const apiMdPath = join(fullPath, `../__docs__/${pathMath[1]}-api.${lang}.md`); + let mdContent = `### ${pathMath[1]}`; + // props + const selfProps = meta.props.filter((prop) => !prop.global); + if (selfProps.length) { + const tableHeader = { + 'zh-CN': ['属性名', '类型', '默认值', '必填', '说明', '其它'], + 'en-US': ['Prop Name', 'Type', 'Default', 'Required', 'Description', 'Other'], + }; + const excludeTag = ['default', 'zh-CN', 'en-US']; + let propsData = selfProps.map((prop) => { + return [ + escapeInlineCode(prop.name), + escapeInlineCode(prop.type), + prop.default || prop.tags.find((tag) => tag.name === 'default')?.text || '', + prop.required ? '🗸' : '', + prop.tags.find((tag) => tag.name === lang)?.text || prop.description || '', + prop.tags + .filter((tag) => !excludeTag.includes(tag.name)) + .map((tag) => `^[${tag.name}]${tagTypes[tag.name] || ''}${tag.text ? `\`${tag.text}\`` : ''}`) + .join(' '), + ]; + }); + propsData.unshift(tableHeader[lang]); + propsData = cleanTableData(propsData); + mdContent = `${mdContent}\n\n#### props\n\n${markdownTable(propsData)}`; + } + // events + if (meta.events.length) { + const tableHeader = { + 'zh-CN': ['事件名', '签名', '说明', '其它'], + 'en-US': ['Event Name', 'Signature', 'Description', 'Other'], + }; + const excludeTag = ['zh-CN', 'en-US']; + let eventsData = meta.events.map((event) => { + return [ + escapeInlineCode(event.name), + escapeInlineCode(event.signature), + event.tags.find((tag) => tag.name === lang)?.text || event.description || '', + event.tags + .filter((tag) => !excludeTag.includes(tag.name)) + .map((tag) => `^[${tag.name}]${tagTypes[tag.name] || ''}${tag.text ? `\`${tag.text}\`` : ''}`) + .join(' '), + ]; + }); + eventsData.unshift(tableHeader[lang]); + eventsData = cleanTableData(eventsData); + mdContent = `${mdContent}\n\n#### events\n\n${markdownTable(eventsData)}`; + } + // slots + if (meta.slots.length) { + const tableHeader = { + 'zh-CN': ['插槽', '签名', '说明'], + 'en-US': ['Slot Name', 'Signature', 'Description'], + }; + let slotsData = meta.slots.map((slot) => { + return [escapeInlineCode(slot.name), escapeInlineCode(slot.type), slot.description]; + }); + slotsData.unshift(tableHeader[lang]); + slotsData = cleanTableData(slotsData); + mdContent = `${mdContent}\n\n#### slots\n\n${markdownTable(slotsData)}`; + } + // expose + if (meta.exposed.length) { + const tableHeader = { + 'zh-CN': ['名称', '类型', '说明'], + 'en-US': ['Name', 'Type', 'Description'], + }; + let exposeData = meta.exposed.map((expose) => { + return [escapeInlineCode(expose.name), escapeInlineCode(expose.type), expose.description]; + }); + exposeData.unshift(tableHeader[lang]); + exposeData = cleanTableData(exposeData); + mdContent = `${mdContent}\n\n#### expose\n\n${markdownTable(exposeData)}`; + } + await fsp.mkdir(dirname(apiMdPath), { recursive: true }).then(() => fsp.writeFile(apiMdPath, mdContent, { encoding: 'utf-8' })); + } + }); + return Promise.all(promises); +}); +promise.finally(() => { + console.timeEnd('GenerateApi done'); +}); diff --git a/packages/docs/scripts/parseSlotsAndExpose.ts b/packages/docs/scripts/parseSlotsAndExpose.ts new file mode 100644 index 0000000000000000000000000000000000000000..505474e8458782181d7b5342e507d5c386edc5bc --- /dev/null +++ b/packages/docs/scripts/parseSlotsAndExpose.ts @@ -0,0 +1,105 @@ +import { Project, SyntaxKind, ScriptTarget, type TypeNode, type JSDoc, type Node} from 'ts-morph'; +import { promises as fsp } from 'node:fs'; +import { parse } from '@vue/compiler-sfc'; +import { join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const tsConfigFilePath = join(fileURLToPath(import.meta.url), '../../../opendesign', 'tsconfig.app.json'); +interface Definition { + name: string; + type: string; + description: string; + tags: { + name: string; + text: string; + }[]; + schema: string; + declarations: Array<{ file: string; range: [number, number] }>; +} +const project = new Project({ + compilerOptions: { + strict: true, + target: ScriptTarget.Latest, + allowJs: true, + lib: ['lib.esnext.d.ts', 'lib.dom.d.ts'], + }, + tsConfigFilePath, +}); +function parseDoc(jsDocs: JSDoc[]) { + const description = jsDocs + .map((doc) => doc.getDescription()) + .join('\n') + .trim(); + const tags: { name: string; text: string }[] = []; + jsDocs.forEach((doc) => { + doc.getTags().forEach((tag) => { + tags.push({ name: tag.getTagName(), text: tag.getText() }); + }); + }); + return { description, tags }; +} +const importReg = /import\([^)]+\)\./g; +function getTypeText(node: Node) { + return node.getType().getApparentType().getText().replace(importReg, ''); +} +function parseTypeArg(typeArg: TypeNode, slots: Definition[]) { + if (typeArg.isKind(SyntaxKind.TypeLiteral)) { + for (const member of typeArg.getMembers()) { + let name = ''; + let type = ''; + let isParsed = false; + if (member.isKind(SyntaxKind.PropertySignature)) { + // 属性签名 propertyName: type + name = member.getName(); + type = getTypeText(member); + isParsed = true; + } else if (member.isKind(SyntaxKind.MethodSignature)) { + // 方法签名 methodName(paramName: type): type + name = member.getName(); + type = getTypeText(member); + isParsed = true; + } else if (member.isKind(SyntaxKind.IndexSignature)) { + // 索引签名 [key: keyType]: type + name = member.getKeyType().getText(); + type = member.getSignature().getDeclaration().getText(); + isParsed = true; + } + if (isParsed) { + const { description, tags } = parseDoc(member.getJsDocs()); + slots.push({ name, type, description, tags, schema: '', declarations: [] }); + } + } + } +} +function parseParams(node: Node, exposes: string[]) { + node + .getType() + .getProperties() + .forEach((prop) => { + exposes.push(prop.getName()); + }); +} +export default async function parseSlotsAndExpose(filePath: string) { + const slots: Definition[] = []; + const exposes: string[] = []; + const content = await fsp.readFile(filePath, 'utf-8'); + const { descriptor } = parse(content); + if (!descriptor.scriptSetup && !descriptor.script) { + return { slots, exposes }; + } + const code = `${descriptor.script?.content || ''}\n;${descriptor.scriptSetup?.content || ''}`; + const s = project.createSourceFile(`${filePath}.script.ts`, code); + + const callExpressions = s.getDescendantsOfKind(SyntaxKind.CallExpression); + for (const callExpression of callExpressions) { + const funName = callExpression.getExpression()?.getText(); + if (funName === 'defineSlots') { + const typeArg = callExpression.getTypeArguments()[0]; + parseTypeArg(typeArg, slots); + } else if (funName === 'defineExpose') { + const parma = callExpression.getArguments()[0]; + parseParams(parma, exposes); + } + } + return { slots, exposes }; +} diff --git a/packages/docs/scrollbar.md b/packages/docs/scrollbar.md deleted file mode 100644 index cd84a21b2b6d7e368d2a3a8ef5c39b7036d37b7b..0000000000000000000000000000000000000000 --- a/packages/docs/scrollbar.md +++ /dev/null @@ -1,15 +0,0 @@ -### scrollbar 刷新的时机 - -- `showType = hover` - -1. 目标容器滚动时; -2. 鼠标 hoverout 时刷新; - -- `showType = always` - -1. 目标容器滚动时; -2. `autoUpdateOnScrollSize=true`时每 1s 后,在 js idle 时间内刷新; - -- `showType = auto` - -1. 目标容器滚动时; diff --git a/packages/docs/select.md b/packages/docs/select.md deleted file mode 100644 index d4d9d55f44e878ef993554e174de1b17628dd189..0000000000000000000000000000000000000000 --- a/packages/docs/select.md +++ /dev/null @@ -1,54 +0,0 @@ -# Select 下拉选择器 - -下拉选择框,支持单选、多选,加载中状态 - -## props - -| name | type | 默认值 | 说明 | -| :---------------- | :------------------------------------------------------------------------------------------------------------------- | :-------------------------- | ---------------------------------------- | -| modelValue | string \| number \| string[] \| number[] \| (string \| number)[] | -- | 必选,下拉框的 v-model 值 | -| defaultValue | string \| number \| string[] \| number[] \| (string \| number)[] | -- | 必选,下拉框的值 | -| color | "normal" \| "primary" \| "success" \| "warning" \| "danger" | 'normal' | 可选,颜色类型 | -| size | 'mini' \| 'small' \| 'medium' \| 'large' | 'medium' | 可选,按钮尺寸 | -| variant | "solid" \| "outline" \| "text" | 'outline' | 可选,按钮类型 | -| loading | boolean | false | 可选,加载状态 | -| disabled | boolean | false | 可选,是否为禁用状态 | -| placeholder | string | -- | 可选,提示文本 | -| multiple | boolean | false | 可选,是否支持多选 | -| maxTagCount | boolean | false | 可选,是否支持多选 | -| clearable | boolean | true | 可选,是否可以清除 | -| trigger | PopupTriggerT | true | 可选,是否可以清除 | -| optionPosition | PopupPositionT | 'bl' | 可选,下拉的位置 | -| optionWidthMode | 'auto' \| 'min-width' \| 'width' | 'min-width' | 可选,下拉的宽度适配方式 | -| optionWrapClass | string | '' | 可选,下拉的自定义类 | -| unmountOnHide | boolean | false | 可选,是否在结束选择时,卸载下拉选项 | -| transition | string | o-fade-up-enter | 可选,过渡动画 | -| beforeSelect | (value: string \| number, currentValue: SelectValueT) => Promise \| boolean \| SelectValueT | -- | 可选,选择前回调,根据返回值判断是否显示 | -| beforeOptionsShow | () => Promise \| boolean | -- | 显示前回调,根据返回值判断是否显示 | -| optionsWrapper | string \| HTMLElement \| null | 'body' | 显示前回调,根据返回值判断是否显示 | -| foldLabel | (tags: Array) => string | -- | 多选超过最大 tag 是,文本显示 | -| showFoldTags | boolean \| 'hover' \| 'click' | 'hover' | 浮层显示收起的多选 tag | -| optionTitle | string | 选项标题(pad、phone 显示) | -| noResponsive | boolean | 下拉浮层是否响应式 | - -## event - -| name | 参数 | 说明 | -| :--------------------- | :--------------------- | :---------------------- | -| change | (value: SelectValueT) | 选择值切换后触发 | -| update:modelValue | (value: SelectValueT ) | 选择值变化触发 | -| options-visible-change | (value: boolean ) | 下拉选项显示/隐藏后触发 | -| clear | (evt: Event ) | 清除下拉框的值后触发 | - -## expose - -## slot - -| name | 说明 | -| :------- | :----------------- | -| tag-fold | 多选时,折叠的文本 | -| arrow | 下拉箭头图标插槽 | -| suffix | 下拉框后缀插槽 | -| default | 下拉选项插槽 | -| empty | 下拉选项为空时插槽 | -| 'name' | 每个下拉选项插槽 | diff --git a/packages/docs/skeleton.md b/packages/docs/skeleton.md deleted file mode 100644 index 4f202e1e35910918504d1a0f8a082f834a8ea0de..0000000000000000000000000000000000000000 --- a/packages/docs/skeleton.md +++ /dev/null @@ -1,29 +0,0 @@ -# Skeleton 骨架屏 - -## skeletonProps - -| name | type | 默认值 | 说明 | -| :-------- | :------ | :----- | ---------------------------------------- | -| loading | boolean | true | 可选,是否显示加载中状态(即展示骨架屏) | -| animation | boolean | false | 可选,是否展示动画 | -| rows | number | 3 | 可选,行数 | - -## skeletonTextProps - -| name | type | 默认值 | 说明 | -| :--- | :----- | :----- | ---------- | -| rows | number | 3 | 可选,行数 | - -## skeletonAvatarProps - -| name | type | 默认值 | 说明 | -| :---- | :----------------------------------- | :------- | -------------- | -| size | 'large'\| 'medium'\| 'small'\|'mini' | 'medium' | 可选,头像尺寸 | -| round | string | 'pill' | 可选,圆角值 | - -## slot - -| name | 参数 | 说明 | -| :------- | :--- | :------------- | -| template | | 骨架屏模板内容 | -| default | | 骨架屏展示内容 | diff --git a/packages/docs/src/App.vue b/packages/docs/src/App.vue new file mode 100644 index 0000000000000000000000000000000000000000..35beb917be3d2a4d9a5f55b655ff7c097ab21c5a --- /dev/null +++ b/packages/docs/src/App.vue @@ -0,0 +1,202 @@ + + + + + diff --git a/packages/portal-ak/src/ak/fonts/HarmonyOS/HarmonyOS_Hans/HarmonyOS_Hans_Medium.woff b/packages/docs/src/assets/fonts/HarmonyOS_Hans_Bold.woff2 similarity index 36% rename from packages/portal-ak/src/ak/fonts/HarmonyOS/HarmonyOS_Hans/HarmonyOS_Hans_Medium.woff rename to packages/docs/src/assets/fonts/HarmonyOS_Hans_Bold.woff2 index f9dbc4c7fd155483540de4918765e7ebeadc68d2..6cb17baa452ae6ee4f688adfa72be2d6c03938e7 100644 Binary files a/packages/portal-ak/src/ak/fonts/HarmonyOS/HarmonyOS_Hans/HarmonyOS_Hans_Medium.woff and b/packages/docs/src/assets/fonts/HarmonyOS_Hans_Bold.woff2 differ diff --git a/packages/portal-ak/src/ak/fonts/HarmonyOS/HarmonyOS_Hans/HarmonyOS_Hans_Medium.woff2 b/packages/docs/src/assets/fonts/HarmonyOS_Hans_Medium.woff2 similarity index 100% rename from packages/portal-ak/src/ak/fonts/HarmonyOS/HarmonyOS_Hans/HarmonyOS_Hans_Medium.woff2 rename to packages/docs/src/assets/fonts/HarmonyOS_Hans_Medium.woff2 diff --git a/packages/portal-ak/src/ak/fonts/HarmonyOS/HarmonyOS_Hans/HarmonyOS_Hans_Regular.woff2 b/packages/docs/src/assets/fonts/HarmonyOS_Hans_Regular.woff2 similarity index 100% rename from packages/portal-ak/src/ak/fonts/HarmonyOS/HarmonyOS_Hans/HarmonyOS_Hans_Regular.woff2 rename to packages/docs/src/assets/fonts/HarmonyOS_Hans_Regular.woff2 diff --git a/packages/docs/src/assets/fonts/HarmonyOS_Sans_Bold_Italic.woff2 b/packages/docs/src/assets/fonts/HarmonyOS_Sans_Bold_Italic.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..5dbbba7a8923c7c2729b2acb3963a25b8d0f3eee Binary files /dev/null and b/packages/docs/src/assets/fonts/HarmonyOS_Sans_Bold_Italic.woff2 differ diff --git a/packages/docs/src/assets/fonts/HarmonyOS_Sans_Medium_Italic.woff2 b/packages/docs/src/assets/fonts/HarmonyOS_Sans_Medium_Italic.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..b6d79fe18a53213f900ac49391f8b85a3af6fb92 Binary files /dev/null and b/packages/docs/src/assets/fonts/HarmonyOS_Sans_Medium_Italic.woff2 differ diff --git a/packages/docs/src/assets/fonts/HarmonyOS_Sans_Regular_Italic.woff2 b/packages/docs/src/assets/fonts/HarmonyOS_Sans_Regular_Italic.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..982b634d67a0c11ab8a1c803ad333e41cf417b5e Binary files /dev/null and b/packages/docs/src/assets/fonts/HarmonyOS_Sans_Regular_Italic.woff2 differ diff --git a/packages/docs/src/assets/images/opendesign-twinkle.mp4 b/packages/docs/src/assets/images/opendesign-twinkle.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..b9dba1517d803ee28551121f2825ea5cadc3786f Binary files /dev/null and b/packages/docs/src/assets/images/opendesign-twinkle.mp4 differ diff --git a/packages/docs/src/assets/images/opendesign.png b/packages/docs/src/assets/images/opendesign.png new file mode 100644 index 0000000000000000000000000000000000000000..dbcb7de3fbdc17e4c3b81a6abd11be1125bf2aed Binary files /dev/null and b/packages/docs/src/assets/images/opendesign.png differ diff --git a/packages/docs/src/assets/images/tree1.png b/packages/docs/src/assets/images/tree1.png new file mode 100644 index 0000000000000000000000000000000000000000..3c93b39d60984981061324ebeb295d1804645ee2 Binary files /dev/null and b/packages/docs/src/assets/images/tree1.png differ diff --git a/packages/docs/src/assets/images/tree2.png b/packages/docs/src/assets/images/tree2.png new file mode 100644 index 0000000000000000000000000000000000000000..997d3dc1ef892eaf029f3e9d12ec5ee2122785ed Binary files /dev/null and b/packages/docs/src/assets/images/tree2.png differ diff --git a/packages/docs/src/assets/images/tree3.png b/packages/docs/src/assets/images/tree3.png new file mode 100644 index 0000000000000000000000000000000000000000..d90287997cbdacbd51d71729865a7c198a85497e Binary files /dev/null and b/packages/docs/src/assets/images/tree3.png differ diff --git a/packages/docs/src/assets/images/tree4.png b/packages/docs/src/assets/images/tree4.png new file mode 100644 index 0000000000000000000000000000000000000000..3bcd2fbf4b9043e66d5f51544e876268488e2a28 Binary files /dev/null and b/packages/docs/src/assets/images/tree4.png differ diff --git a/packages/docs/src/assets/images/tree5.png b/packages/docs/src/assets/images/tree5.png new file mode 100644 index 0000000000000000000000000000000000000000..4bb087d03cdeaf0e94c83ef493b1135a97197488 Binary files /dev/null and b/packages/docs/src/assets/images/tree5.png differ diff --git a/packages/docs/src/assets/style/font-family.scss b/packages/docs/src/assets/style/font-family.scss new file mode 100644 index 0000000000000000000000000000000000000000..6bfc052d6e6516a49cadee76fdd4112dc660e6ca --- /dev/null +++ b/packages/docs/src/assets/style/font-family.scss @@ -0,0 +1,61 @@ +@font-face { + font-display: swap; + font-family: HarmonyOS-Hans; + font-style: normal; + font-weight: 400; + src: + local('HarmonyOSHans'), + // 将字体文件从 public/ 中移到 src/assets/fonts/ 文件夹中 + // 根据当前的 nginx 配置,src/assets/ 中的文件有缓存,public/ 文件夹中的文件不会被缓存 + url(../fonts/HarmonyOS_Hans_Regular.woff2) format('woff2'); +} + +@font-face { + font-display: swap; + font-family: HarmonyOS-Hans; + font-style: normal; + font-weight: 500; + src: + local('HarmonyOSHans Medium'), + url(../fonts/HarmonyOS_Hans_Medium.woff2) format('woff2'); +} + +@font-face { + font-display: swap; + font-family: HarmonyOS-Hans; + font-style: normal; + font-weight: 700; + src: + local('HarmonyOSHans Bold'), + url(../fonts/HarmonyOS_Hans_Bold.woff2) format('woff2'); +} + +@font-face { + font-display: swap; + font-family: HarmonyOS-Hans; + font-style: italic; + font-weight: 400; + src: + local('HarmonyOSSans'), + url(../fonts/HarmonyOS_Sans_Regular_Italic.woff2) format('woff2'); +} + +@font-face { + font-display: swap; + font-family: HarmonyOS-Hans; + font-style: italic; + font-weight: 500; + src: + local('HarmonyOSSans Medium'), + url(../fonts/HarmonyOS_Sans_Medium_Italic.woff2) format('woff2'); +} + +@font-face { + font-display: swap; + font-family: HarmonyOS-Hans; + font-style: italic; + font-weight: 700; + src: + local('HarmonyOSSans Bold'), + url(../fonts/HarmonyOS_Sans_Bold_Italic.woff2) format('woff2'); +} diff --git a/packages/docs/src/assets/style/index.ts b/packages/docs/src/assets/style/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..ea24668d1dc9a65e418f117d3ed4ba24503a232d --- /dev/null +++ b/packages/docs/src/assets/style/index.ts @@ -0,0 +1,5 @@ +import '@opensig/opendesign/theme/grid.scss'; +import './font-family.scss'; +import './var-map.scss'; +import './style.scss'; +import './markdown.scss'; diff --git a/packages/docs/src/assets/style/markdown.scss b/packages/docs/src/assets/style/markdown.scss new file mode 100644 index 0000000000000000000000000000000000000000..be0e62e1c9bea44bfc339ac6ece10d776d32b111 --- /dev/null +++ b/packages/docs/src/assets/style/markdown.scss @@ -0,0 +1,119 @@ +:not(pre) > code { + border-radius: 4px; + padding: 0.1em 0.2em; + margin: 0 0.2em; + background-color: var(--o-color-control1-light); +} + +[data-o-theme$='light'] pre span { + color: var(--shiki-light); +} + +[data-o-theme$='dark'] pre span { + color: var(--shiki-dark); +} +p + .code-container, +p + .o-table { + margin-top: 8px; +} +ol, +ul, +li { + margin: 0; + padding: 0; +} +li { + .code-container { + margin-top: 4px; + } +} +@include respond-to('>pad_v') { + ol { + padding-inline-start: calc(1em + 24px); + list-style-type: decimal; + } + ul { + padding-inline-start: calc(1em + 24px); + list-style-type: disc; + } + ul, + ol { + li + li { + margin-block-start: 4px; + } + ul { + list-style-type: circle; + padding-inline-start: calc(0.25em + 24px); + } + ol { + padding-inline-start: 24px; + } + ul, + ol { + li { + margin-block-start: 0px; + } + ul { + list-style-type: square; + } + } + } +} +@include respond-to('<=pad_v') { + ol { + padding-inline-start: 1.6em; + list-style-type: decimal; + } + ul { + padding-inline-start: 1.25em; + list-style-type: disc; + } + ul, + ol { + li { + margin-top: 4px; + } + ul { + list-style-type: circle; + } + ul, + ol { + li { + margin-top: 0px; + } + ul { + list-style-type: square; + } + } + } +} + +.markdown-body { + padding: 0 var(--o3-gap-5); + .markdown-body { + padding: 0; + } + .o-table-wrap { + overflow-x: auto; + th:first-child, + td:first-child { + position: sticky; + left: 0; + } + td:first-child { + background-color: var(--table-bg-color); + } + @media (hover: hover) { + tr:hover td { + background-color: var(--table-row-hover); + } + } + } + @include respond-to('<=pad_v') { + padding: 0; + } +} + +.tooltip { + cursor: help; +} diff --git a/packages/docs/src/assets/style/style.scss b/packages/docs/src/assets/style/style.scss new file mode 100644 index 0000000000000000000000000000000000000000..f78d96566cf65e41410a3040f749cf4c1da7d649 --- /dev/null +++ b/packages/docs/src/assets/style/style.scss @@ -0,0 +1,134 @@ +* { + box-sizing: border-box; +} +body { + // 采用鸿蒙字体 + font-family: 'HarmonyOS-Hans', Arial, 'Microsoft YaHei', sans-serif; + font-size: var(--o3-font_size-text1); + line-height: var(--o3-line_height-text1); + font-weight: 400; + + color-scheme: light dark; + + color: var(--o-color-info2); + background-color: var(--o-color-fill1); + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + -webkit-text-size-adjust: 100%; +} + +body { + margin: 0; +} +code { + // 代码中使用等宽字体 + font-family: 'Cascadia Code', 'SF Mono', 'Menlo', 'Consolas', 'DejaVu Sans Mono', 'Ubuntu Mono', monospace; +} + +p, +ol, +ul, +h1, +h2, +h3, +h4, +h5, +h6, +.code-container { + margin-top: var(--o3-gap-4); + margin-bottom: var(--o3-gap-1); +} + +h1 { + font-size: var(--o3-font_size-h1); + line-height: var(--o3-line_height-h1); +} +h2, h3 { + font-size: var(--o3-font_size-h3); + line-height: var(--o3-line_height-h3); +} +h4, h5 { + font-size: var(--o-font_size-text2); + line-height: var(--o3-line_height-text2); +} +h6 { + font-size: var(--o-font_size-text1); + line-height: var(--o3-line_height-text1); +} +p { + margin-top: var(--o3-gap-2); +} +.page-demo { + padding: var(--o3-gap-2) var(--o3-gap-5); + h4 { + color: var(--o-color-info1); + } +} +.col { + display: flex; + align-items: center; + justify-content: flex-start; + flex-wrap: wrap; + gap: var(--o3-gap-2); +} +.block { + padding: var(--o3-gap-4); + border: 1px solid var(--o-color-control1-light); +} +.row { + display: flex; + align-items: center; + justify-content: flex-start; + flex-wrap: wrap; + gap: var(--o3-gap-3); + & + .row { + margin-top: var(--o3-gap-6); + } +} +section { + border: 1px solid var(--o-color-control1-light); + padding: var(--o3-gap-4); + + h4 { + text-align: center; + margin: 0; + margin-bottom: var(--o3-gap-3); + } + + section { + margin-top: var(--o3-gap-3); + } + .row { + width: 100%; + } +} +.col-2 { + width: 50%; + padding: var(--o3-gap-5); + + .col-2 { + margin-left: var(--o3-gap-3); + } +} +section { + display: flex; + align-items: center; + justify-content: flex-start; + flex-wrap: wrap; + gap: var(--o3-gap-4); + &[block] { + display: block; + } +} + +.btn { + padding: var(--o3-gap-1) var(--o3-gap-4); + border: 1px solid #ccc; + display: inline-flex; + border-radius: 4px; + cursor: pointer; + &:hover { + border-color: #aaa; + } +} diff --git a/packages/docs/src/assets/style/var-map.scss b/packages/docs/src/assets/style/var-map.scss new file mode 100644 index 0000000000000000000000000000000000000000..33834dc0b74e91f46bbb606434ef0b6959084e6c --- /dev/null +++ b/packages/docs/src/assets/style/var-map.scss @@ -0,0 +1,190 @@ +:root { + @media (min-width: 1681px) { + // 文字 + --o3-font_size-display1: var(--o-font_size-display1); + --o3-line_height-display1: var(--o-line_height-display1); + --o3-font_size-display2: var(--o-font_size-display2); + --o3-line_height-display2: var(--o-line_height-display2); + // 楼层标题 + --o3-font_size-display3: var(--o-font_size-display3); + --o3-line_height-display3: var(--o-line_height-display3); + --o3-font_size-h1: var(--o-font_size-h1); + --o3-line_height-h1: var(--o-line_height-h1); + --o3-font_size-h2: var(--o-font_size-h2); + --o3-line_height-h2: var(--o-line_height-h2); + --o3-font_size-h3: var(--o-font_size-h3); + --o3-line_height-h3: var(--o-line_height-h3); + --o3-font_size-h4: var(--o-font_size-h4); + --o3-line_height-h4: var(--o-line_height-h4); + // 正文 + --o3-font_size-text2: var(--o-font_size-text2); + --o3-line_height-text2: var(--o-line_height-text2); + --o3-font_size-text1: var(--o-font_size-text1); + --o3-line_height-text1: var(--o-line_height-text1); + // 提示文本 + --o3-font_size-tip1: var(--o-font_size-tip1); + --o3-line_height-tip1: var(--o-line_height-tip1); + --o3-font_size-tip2: var(--o-font_size-tip2);; + --o3-line_height-tip2: var(--o-line_height-tip2); + + // 间距 + --o3-gap-10: var(--o-gap-10); + --o3-gap-9: var(--o-gap-9); + --o3-gap-8: var(--o-gap-8); + --o3-gap-7: var(--o-gap-7); + --o3-gap-6: var(--o-gap-6); + --o3-gap-5: var(--o-gap-5); + --o3-gap-4: var(--o-gap-4); + --o3-gap-3: var(--o-gap-3); + --o3-gap-2: var(--o-gap-2); + --o3-gap-1: var(--o-gap-1); + + // 图标大小 + --o3-icon_size-4xl: var(--o-icon_size-4xl); + --o3-icon_size-3xl: var(--o-icon_size-3xl); + --o3-icon_size-2xl: var(--o-icon_size-2xl); + --o3-icon_size-xl: var(--o-icon_size-xl); + --o3-icon_size-l: var(--o-icon_size-l); + --o3-icon_size-m: var(--o-icon_size-m); + --o3-icon_size-s: var(--o-icon_size-s); + --o3-icon_size-xs: var(--o-icon_size-xs); + } + @media (min-width: 1201px) and (max-width: 1680px) { + // 文字 + --o3-font_size-display1: var(--o-font_size-display2); + --o3-line_height-display1: var(--o-line_height-display2); + --o3-font_size-display2: var(--o-font_size-display3); + --o3-line_height-display2: var(--o-line_height-display3); + // 楼层标题 + --o3-font_size-display3: var(--o-font_size-h1); + --o3-line_height-display3: var(--o-line_height-h1); + --o3-font_size-h1: var(--o-font_size-h4); + --o3-line_height-h1: var(--o-line_height-h4); + --o3-font_size-h2: var(--o-font_size-h4); + --o3-line_height-h2: var(--o-line_height-h4); + --o3-font_size-h3: var(--o-font_size-text2); + --o3-line_height-h3: var(--o-line_height-text2); + --o3-font_size-h4: var(--o-font_size-text2); + --o3-line_height-h4: var(--o-line_height-text2); + // 正文 + --o3-font_size-text2: var(--o-font_size-text1); + --o3-line_height-text2: var(--o-line_height-text1); + --o3-font_size-text1: var(--o-font_size-tip1); + --o3-line_height-text1: var(--o-line_height-tip1); + // 提示文本 + --o3-font_size-tip1: var(--o-font_size-tip1); + --o3-line_height-tip1: var(--o-line_height-tip1); + --o3-font_size-tip2: var(--o-font_size-tip2); + --o3-line_height-tip2: var(--o-line_height-tip2); + + // 间距 + --o3-gap-10: 56px; + --o3-gap-9: 48px; + --o3-gap-8: 40px; + --o3-gap-7: 24px; + --o3-gap-6: 24px; + --o3-gap-5: 16px; + --o3-gap-4: 12px; + --o3-gap-3: 8px; + --o3-gap-2: 8px; + --o3-gap-1: 4px; + + // 图标大小 + --o3-icon_size-4xl: var(--o-icon_size-3xl); + --o3-icon_size-3xl: var(--o-icon_size-2xl); + --o3-icon_size-2xl: var(--o-icon_size-xl); + --o3-icon_size-xl: var(--o-icon_size-l); + --o3-icon_size-l: var(--o-icon_size-l); + --o3-icon_size-m: var(--o-icon_size-m); + --o3-icon_size-s: var(--o-icon_size-s); + --o3-icon_size-xs: var(--o-icon_size-xs); + } + @include respond-to('<=pad') { + // 文字 + --o3-font_size-display1: var(--o-font_size-display3); + --o3-line_height-display1: var(--o-line_height-display3); + --o3-font_size-display2: var(--o-font_size-h1); + --o3-line_height-display2: var(--o-line_height-h1); + // 楼层标题 + --o3-font_size-display3: var(--o-font_size-h2); + --o3-line_height-display3: var(--o-line_height-h2); + --o3-font_size-h1: var(--o-font_size-h4); + --o3-line_height-h1: var(--o-line_height-h4); + --o3-font_size-h2: var(--o-font_size-text2); + --o3-line_height-h2: var(--o-line_height-text2); + --o3-font_size-h3: var(--o-font_size-text1); + --o3-line_height-h3: var(--o-line_height-text1); + --o3-font_size-h4: var(--o-font_size-text1); + --o3-line_height-h4: var(--o-line_height-text1); + // 正文 + --o3-font_size-text2: var(--o-font_size-tip1); + --o3-line_height-text2: var(--o-line_height-tip1); + --o3-font_size-text1: var(--o-font_size-tip1); + --o3-line_height-text1: var(--o-line_height-tip1); + // 提示文本 + --o3-font_size-tip1: var(--o-font_size-tip2); + --o3-line_height-tip1: var(--o-line_height-tip2); + --o3-font_size-tip2: var(--o-font_size-tip2); + --o3-line_height-tip2: var(--o-line_height-tip2); + + // 间距 + --o3-gap-10: 40px; + --o3-gap-9: 32px; + --o3-gap-8: 24px; + --o3-gap-7: 16px; + --o3-gap-6: 16px; + --o3-gap-5: 12px; + --o3-gap-4: 8px; + --o3-gap-3: 8px; + --o3-gap-2: 8px; + --o3-gap-1: 4px; + + // 图标大小 + --o3-icon_size-4xl: var(--o-icon_size-2xl); + --o3-icon_size-3xl: var(--o-icon_size-xl); + --o3-icon_size-2xl: var(--o-icon_size-xl); + --o3-icon_size-xl: var(--o-icon_size-l); + --o3-icon_size-l: var(--o-icon_size-m); + --o3-icon_size-m: var(--o-icon_size-m); + --o3-icon_size-s: var(--o-icon_size-s); + --o3-icon_size-xs: var(--o-icon_size-xs); + } + @include respond-to('<=pad_v') { + // 文字 + --o3-font_size-display1: var(--o-font_size-h3); + --o3-line_height-display1: var(--o-line_height-h3); + --o3-font_size-display2: var(--o-font_size-h4); + --o3-line_height-display2: var(--o-line_height-h4); + // 楼层标题 + --o3-font_size-display3: var(--o-font_size-text2); + --o3-line_height-display3: var(--o-line_height-text2); + --o3-font_size-h1: var(--o-font_size-text2); + --o3-line_height-h1: var(--o-line_height-text2); + --o3-font_size-h2: var(--o-font_size-text1); + --o3-line_height-h2: var(--o-line_height-text1); + --o3-font_size-h3: var(--o-font_size-text1); + --o3-line_height-h3: var(--o-line_height-text1); + --o3-font_size-h4: var(--o-font_size-text1); + --o3-line_height-h4: var(--o-line_height-text1); + // 正文 + --o3-font_size-text2: var(--o-font_size-tip1); + --o3-line_height-text2: var(--o-line_height-tip1); + --o3-font_size-text1: var(--o-font_size-tip1); + --o3-line_height-text1: var(--o-line_height-tip1); + // 提示文本 + --o3-font_size-tip1: var(--o-font_size-tip2); + --o3-line_height-tip1: var(--o-line_height-tip2); + --o3-font_size-tip2: 10px; + --o3-line_height-tip2: 16px; + + // 图标大小 + --o3-icon_size-4xl: var(--o-icon_size-xl); + --o3-icon_size-3xl: var(--o-icon_size-xl); + --o3-icon_size-2xl: var(--o-icon_size-l); + --o3-icon_size-xl: var(--o-icon_size-l); + --o3-icon_size-l: var(--o-icon_size-m); + --o3-icon_size-m: var(--o-icon_size-m); + --o3-icon_size-s: var(--o-icon_size-m); + --o3-icon_size-xs: var(--o-icon_size-xs); + } +} \ No newline at end of file diff --git a/packages/docs/src/components/CodeContainer.vue b/packages/docs/src/components/CodeContainer.vue new file mode 100644 index 0000000000000000000000000000000000000000..2bc9674453f9a385492fbe6434dfb55c1711f9d6 --- /dev/null +++ b/packages/docs/src/components/CodeContainer.vue @@ -0,0 +1,104 @@ + + + + + diff --git a/packages/docs/src/components/DemoContainer.vue b/packages/docs/src/components/DemoContainer.vue new file mode 100644 index 0000000000000000000000000000000000000000..4fe3975930ff8ad7703d3ec1b41daf213d2e2a82 --- /dev/null +++ b/packages/docs/src/components/DemoContainer.vue @@ -0,0 +1,102 @@ + + + + diff --git a/packages/docs/src/components/DemoUsage.vue b/packages/docs/src/components/DemoUsage.vue new file mode 100644 index 0000000000000000000000000000000000000000..728373183f7193a9bf9787b2b3abe2e4df545534 --- /dev/null +++ b/packages/docs/src/components/DemoUsage.vue @@ -0,0 +1,237 @@ + + + diff --git a/packages/docs/src/components/DocConfigProvide.vue b/packages/docs/src/components/DocConfigProvide.vue new file mode 100644 index 0000000000000000000000000000000000000000..6e202e4ca746e470ed695c62d1ffb54e5bb69b80 --- /dev/null +++ b/packages/docs/src/components/DocConfigProvide.vue @@ -0,0 +1,32 @@ + + diff --git a/packages/docs/src/components/DocLink.vue b/packages/docs/src/components/DocLink.vue new file mode 100644 index 0000000000000000000000000000000000000000..9055a79b995ba42e8d11a77083dc934e0ec0c9f9 --- /dev/null +++ b/packages/docs/src/components/DocLink.vue @@ -0,0 +1,37 @@ + + diff --git a/packages/docs/src/components/OperatorView.ts b/packages/docs/src/components/OperatorView.ts new file mode 100644 index 0000000000000000000000000000000000000000..b5d4c4bf04327833ca98b1b603bc2bcf8860a72f --- /dev/null +++ b/packages/docs/src/components/OperatorView.ts @@ -0,0 +1,184 @@ +import { defineComponent, h, Fragment, type PropType, type VNode } from 'vue'; +import { OSelect, OOption, OInput, OInputNumber, OCheckbox, OCheckboxGroup, OTextarea, ORadio, ORadioGroup } from '@opensig/opendesign'; + +export type CheckboxScheme = { + type: 'boolean'; + default?: boolean; + label?: string; + disabled?: boolean; +}; +export type SelectorScheme = { + type: 'list'; + list: Array; + default?: string | number; + label?: string; + disabled?: boolean; +}; +export type InputScheme = { + type: 'string'; + default?: string; + label?: string; + disabled?: boolean; +}; +export type TextareaScheme = { + type: 'textarea'; + default?: string; + label?: string; + row?: number; + disabled?: boolean; +}; +export type InputNumberScheme = { + type: 'number'; + step?: number; + min?: number; + max?: number; + default?: number; + label?: string; + disabled?: boolean; +}; +export type RadioScheme = { + type: 'radio'; + default?: string | number; + list: Array; + disabled?: boolean; +}; +export type SchemeT = CheckboxScheme | SelectorScheme | InputScheme | TextareaScheme | InputNumberScheme | RadioScheme; +export type State = Record; + +const camelcase2words = (str: string) => str.replace(/(?<=[a-z])([A-Z])|(?<=[A-Z])([A-Z][a-z])/g, ' $&').replace(/^[a-z]/, (char) => char.toUpperCase()); +const createCheckboxItem = (key: string, value: CheckboxScheme) => { + return h(OCheckbox, { value: key, disabled: value.disabled }, { default: () => value.label || camelcase2words(key) }); +}; +const createSelectorItem = (key: string, value: SelectorScheme, state: State) => { + return h(Fragment, [ + h('span', { class: 'props-playground-selector-name' }, value.label || camelcase2words(key)), + h( + OSelect, + { modelValue: state[key], disabled: value.disabled, 'onUpdate:modelValue': (val) => (state[key] = val) }, + { + default: () => value.list.map((item) => h(OOption, { value: item, label: `${item}` })), + }, + ), + ]); +}; +const createInputItem = (key: string, value: InputScheme, state: State) => { + return h(Fragment, [ + h('span', { class: 'props-playground-selector-name' }, value.label || camelcase2words(key)), + h(OInput, { modelValue: state[key], disabled: value.disabled, 'onUpdate:modelValue': (val) => (state[key] = val) }), + ]); +}; +const createTextareaItem = (key: string, value: TextareaScheme, state: State) => { + return h(Fragment, [ + h('span', { class: 'props-playground-selector-name' }, value.label || camelcase2words(key)), + h(OTextarea, { + modelValue: state[key], + disabled: value.disabled, + style: { '--row': value.row || 3 }, + class: 'props-playground-textarea', + 'onUpdate:modelValue': (val) => (state[key] = val), + }), + ]); +}; +const createInputNumberItem = (key: string, value: InputNumberScheme, state: State) => { + return h(Fragment, [ + h('span', { class: 'props-playground-selector-name' }, value.label || camelcase2words(key)), + h(OInputNumber, { + modelValue: state[key], + disabled: value.disabled, + min: value.min, + max: value.max, + step: value.step, + 'onUpdate:modelValue': (val) => (state[key] = val), + }), + ]); +}; +const createRadioItem = (key: string, value: RadioScheme, state: State) => { + return h( + ORadioGroup, + { modelValue: state[key], disabled: value.disabled, class: 'radio-group', 'onUpdate:modelValue': (val) => (state[key] = val) }, + { default: () => value.list.map((item) => h(ORadio, { value: item }, { default: () => item })) }, + ); +}; +/** 表单控件组件 */ +export default defineComponent({ + name: 'OperatorView', + props: { + schema: { + type: Object as PropType>, + required: true, + }, + state: { + type: Object as PropType, + required: true, + }, + checkboxGroupValue: { + type: Array as PropType>, + required: true, + }, + }, + setup(props) { + return () => { + /** 复选框控件 */ + const checkboxGroup: VNode[] = []; + /** 选着框控件 */ + const selectionOrInputGroup: VNode[] = []; + /** 单选框控件 */ + const radioGroup: VNode[] = []; + const operatorGroup: VNode[] = []; + + Object.entries(props.schema).forEach(([key, value]) => { + switch (value.type) { + case 'boolean': + checkboxGroup.push(createCheckboxItem(key, value)); + break; + case 'list': + selectionOrInputGroup.push(createSelectorItem(key, value, props.state)); + break; + case 'string': + selectionOrInputGroup.push(createInputItem(key, value, props.state)); + break; + case 'textarea': + selectionOrInputGroup.push(createTextareaItem(key, value, props.state)); + break; + case 'number': + selectionOrInputGroup.push(createInputNumberItem(key, value, props.state)); + break; + case 'radio': + radioGroup.push(createRadioItem(key, value, props.state)); + break; + } + }); + if (radioGroup.length) { + operatorGroup.push(h(Fragment, radioGroup)); + } + if (checkboxGroup.length) { + operatorGroup.push( + h( + OCheckboxGroup, + { + class: 'checkbox-group', + modelValue: props.checkboxGroupValue, + onChange: (val) => { + Object.entries(props.schema).forEach(([key, value]) => { + if (value.type === 'boolean') { + props.state[key] = false; + } + }); + val.forEach((name) => { + props.state[name] = true; + }); + }, + }, + { + default: () => checkboxGroup, + }, + ), + ); + } + if (selectionOrInputGroup.length) { + operatorGroup.push(h('div', { class: 'operator-group' }, selectionOrInputGroup)); + } + return h(Fragment, operatorGroup); + }; + }, +}); diff --git a/packages/docs/src/components/TheAnchor.ts b/packages/docs/src/components/TheAnchor.ts new file mode 100644 index 0000000000000000000000000000000000000000..8741c03e9e775af71f704cd55bdf3141bea09521 --- /dev/null +++ b/packages/docs/src/components/TheAnchor.ts @@ -0,0 +1,102 @@ +import { defineComponent, h, VNode, type PropType, type ComponentPublicInstance } from 'vue'; +import { OAnchor, OAnchorItem } from '@opensig/opendesign'; + +type Header = { title: string; level: number; id: string; children?: Header[]; parent?: Header }; +/** + * 将锚点数据转换为虚拟节点 + * @param heads 锚点数据,数据结构为树状 + * @returns 虚拟节点数组,供OAnchor渲染 + */ +const createAnchorItems = (heads: Header[]): VNode[] => { + const children: VNode[] = []; + for (const item of heads) { + if (item.children) { + const grandChildren = createAnchorItems(item.children); + children.push(h(OAnchorItem, { href: `#${item.id}`, title: item.title }, { default: () => grandChildren })); + } else { + children.push(h(OAnchorItem, { href: `#${item.id}`, title: item.title })); + } + } + return children; +}; +/** + * TheAnchor组件,传入Array<{ title: string; level: number; id: string }>,使用OAnchor与OAnchorItem渲染锚点 + */ +export default defineComponent({ + name: 'TheAnchor', + props: { + /** 锚点数据 */ + heads: { + type: Array as PropType>, + default: () => [], + }, + /** 滚动偏移量 */ + targetOffset: { + type: Number, + default: 0, + }, + }, + + setup({ heads, targetOffset }) { + let anchorContainer: HTMLElement | null = null; + + const setAnchorContainer = (inst: ComponentPublicInstance | Element | null) => { + if (!inst) { + return; + } + if (inst instanceof Element) { + anchorContainer = inst as HTMLElement; + } else if (inst.$el) { + anchorContainer = inst.$el; + } + }; + const handleAnchorChange = (href: string) => { + anchorContainer?.querySelector(`.o-anchor-item-link[href="${href}"]`)?.scrollIntoView({ + behavior: 'smooth', + block: 'nearest', + }); + }; + return () => { + // OAnchor 的默认插槽回调函数。 + // heads 变量不应在 TheAnchor 组件中访问,这会造成 TheAnchor + OAnchor 同时组件更新, + // 而是应该封装在插槽函数中这样只更新 OAnchor 组件 + const anchorDefaultSlot = () => { + if (!heads.length) { + return null; + } + const root: Header = { + title: '', + level: 0, + id: '', + }; + let current: Header = root; + // 锚点数据从平铺结构构造为树状结构,以便生成VNode + for (const _item of heads) { + // 浅拷贝数据,避免此处修改响应式对象而造成组件意外更新 + const item = { ..._item } as Header; + if (item.level > current.level) { + current.children = current.children || []; + current.children.push(item); + item.parent = current; + current = item; + } else { + while (current.level > item.level && current.parent) { + current = current.parent; + } + if (current && current.parent) { + current.parent.children = current.parent.children || []; + current.parent.children.push(item); + item.parent = current.parent; + current = item; + } + } + } + if (!root.children) { + return null; + } + return createAnchorItems(root.children); + }; + return h(OAnchor, { targetOffset, onChange: handleAnchorChange, ref: setAnchorContainer }, { default: anchorDefaultSlot }); + }; + }, +}); diff --git a/packages/docs/src/components/TheAside.vue b/packages/docs/src/components/TheAside.vue new file mode 100644 index 0000000000000000000000000000000000000000..293ae9e126d81d9e4934e44a3e0a236053fef6aa --- /dev/null +++ b/packages/docs/src/components/TheAside.vue @@ -0,0 +1,94 @@ + + + diff --git a/packages/docs/src/components/TheHeader.vue b/packages/docs/src/components/TheHeader.vue new file mode 100644 index 0000000000000000000000000000000000000000..e16c2e39acd2000cded12e314552a85583fd2b2f --- /dev/null +++ b/packages/docs/src/components/TheHeader.vue @@ -0,0 +1,168 @@ + + + diff --git a/packages/docs/src/globalComponents.d.ts b/packages/docs/src/globalComponents.d.ts new file mode 100644 index 0000000000000000000000000000000000000000..b46c701b3cdff7fe7d5282feb4b901b7a16f65f4 --- /dev/null +++ b/packages/docs/src/globalComponents.d.ts @@ -0,0 +1,11 @@ +export const CodeContainer: (typeof import('@/components/CodeContainer.vue'))['default']; +export const DemoContainer: (typeof import('@/components/DemoContainer.vue'))['default']; +export const DemoUsage: (typeof import('@/components/DemoUsage.vue'))['default']; + +declare module 'vue' { + export interface GlobalComponents { + CodeContainer: (typeof import('@/components/CodeContainer.vue'))['default']; + DemoContainer: (typeof import('@/components/DemoContainer.vue'))['default']; + DemoUsage: (typeof import('@/components/DemoUsage.vue'))['default']; + } +} diff --git a/packages/docs/src/lang/en-US.ts b/packages/docs/src/lang/en-US.ts new file mode 100644 index 0000000000000000000000000000000000000000..2ff931a6dd9a37c36aa3a1cae04b51397310a11c --- /dev/null +++ b/packages/docs/src/lang/en-US.ts @@ -0,0 +1,16 @@ +export default { + locale: 'en-US', + 'components.component': 'Component', + 'header.globalSettings': 'Global settings', + 'header.home': 'Home', + 'header.theme': 'Theme', + 'dev.devEnv': 'dev env', + 'menu.nav': 'Navigation component', + 'menu.operator': 'Operation component', + 'menu.input': 'Input component', + 'menu.feedback': 'Feedback component', + 'menu.container': 'Container component', + 'menu.display': 'Display component', + notFound: 'Page not found', + goBackHome: 'Go back home', +}; diff --git a/packages/docs/src/lang/index.ts b/packages/docs/src/lang/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..a793f3b53e3956a562eb0df6c4b522793e74906b --- /dev/null +++ b/packages/docs/src/lang/index.ts @@ -0,0 +1,37 @@ +import { useLocale, useI18n, addLocale } from '@opensig/opendesign'; +import { computed } from 'vue'; +// @opensig/opendesign 未暴露语言包,因此通过路径添加 +import enUS from '@opensig/opendesign/locale/lang/en-us'; +import enUSPortal from './en-US'; +import zhCNPortal from './zh-CN'; + +addLocale(enUS, { overwrite: true }); +addLocale(enUSPortal, { overwrite: true }); +addLocale(zhCNPortal, { overwrite: true }); + + + +export type LocaleT = 'zh-CN' | 'en-US'; +type LocaleItemT = { value: LocaleT; label: string }; +export const locales: Array = [ + { + value: 'zh-CN', + label: '中文', + }, + { + value: 'en-US', + label: 'English', + }, +]; +export const LOCALE_COOKIE_KEY = 'locale'; + +export const currentLocale = computed(() => { + const { locale } = useI18n(); + const rlt = locales.find((item) => item.value === locale.value); + return rlt || locales[0]; +}); +export const changeLocale = (l: LocaleT) => { + useLocale(l); + document.cookie = `${LOCALE_COOKIE_KEY}=${l}; path=/`; +}; + diff --git a/packages/docs/src/lang/zh-CN.ts b/packages/docs/src/lang/zh-CN.ts new file mode 100644 index 0000000000000000000000000000000000000000..fbe28203fbbf19d5867f6e2152938baceeb7a6c4 --- /dev/null +++ b/packages/docs/src/lang/zh-CN.ts @@ -0,0 +1,16 @@ +export default { + locale: 'zh-CN', + 'components.component': '组件', + 'header.globalSettings': '全局设置', + 'header.home': '首页', + 'header.theme': '主题', + 'dev.devEnv': '开发环境', + 'menu.nav': '导航组件', + 'menu.operator': '操作组件', + 'menu.input': '输入组件', + 'menu.feedback': '反馈组件', + 'menu.container': '容器组件', + 'menu.display': '展示组件', + notFound: '页面走丢了', + goBackHome: '返回首页', +}; diff --git a/packages/docs/src/main.ts b/packages/docs/src/main.ts new file mode 100644 index 0000000000000000000000000000000000000000..abe27830674f80a4e52bf240e64495b118480a48 --- /dev/null +++ b/packages/docs/src/main.ts @@ -0,0 +1,43 @@ +import { createApp } from 'vue'; +import { createPinia } from 'pinia'; +import 'normalize.css'; + +import '@/assets/style/index'; + +import { router } from '@/router/index'; + +import App from './App.vue'; +import * as Opendesign from '@opensig/opendesign'; +import * as DocIcons from '@/icon-components'; +import CodeContainer from './components/CodeContainer.vue'; +import DemoContainer from './components/DemoContainer.vue'; +import DemoUsage from './components/DemoUsage.vue'; +import DocLink from './components/DocLink.vue'; + +const app = createApp(App); +const pinia = createPinia(); + +app.use(pinia); +app.use(router); +app.component('CodeContainer', CodeContainer); +app.component('DemoContainer', DemoContainer); +app.component('DemoUsage', DemoUsage); +Object.entries(Opendesign).forEach(([name, value]) => { + if (typeof value !== 'object' || !value) { + return; + } + // 将所有组件全局注册,以便 md 文件使用 + if (typeof (value as any).install === 'function') { + app.use(value as any); + } + if (name.startsWith('OIcon') && name.length > 5) { + app.component(name, value as any); + } +}); +Object.entries(DocIcons).forEach(([name, value]) => { + app.component(name, value as any); +}); +// 某些组件有问题,需要手动注册 +app.component('OCarouselItem', Opendesign.OCarouselItem); +app.component('DocLink', DocLink); +app.mount('#app'); diff --git a/packages/docs/src/pages/NotFound.vue b/packages/docs/src/pages/NotFound.vue new file mode 100644 index 0000000000000000000000000000000000000000..382a1203963fb2d65ceebe02578f7de2bf9b3dd9 --- /dev/null +++ b/packages/docs/src/pages/NotFound.vue @@ -0,0 +1,19 @@ + + + diff --git a/packages/portal-ak/src/pages/TheHome.vue b/packages/docs/src/pages/TheHome.vue similarity index 66% rename from packages/portal-ak/src/pages/TheHome.vue rename to packages/docs/src/pages/TheHome.vue index e47a830af4bf0e0966cfd7c43f431794d8c71683..a37ec7b6ba91444e97186cacb290fe15d5e14b82 100644 --- a/packages/portal-ak/src/pages/TheHome.vue +++ b/packages/docs/src/pages/TheHome.vue @@ -1,12 +1,10 @@ diff --git a/packages/docs/src/router/index.ts b/packages/docs/src/router/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..4b49d0d6d58a3b02c062c86964bd9744d412ef27 --- /dev/null +++ b/packages/docs/src/router/index.ts @@ -0,0 +1,36 @@ +import { createRouter, createWebHistory } from 'vue-router'; +import TheHome from '../pages/TheHome.vue'; +import { routes as componentRoutes } from './components'; +import { useI18n } from '@opensig/opendesign'; + +export const router = createRouter({ + history: createWebHistory(), + routes: [ + { + path: '/', + name: 'Home', + component: TheHome, + }, + ...componentRoutes, + { path: '/:pathMatch(.*)*', name: 'NotFound', component: () => import('@/pages/NotFound.vue') }, + ], +}); +export type MetaT = { sidebar: string; lang: string; kind: string; sidebarName: string }; +export type RouteT = { + path: string; + meta: MetaT; +}; +export type SidebarItemT = { + routes: RouteT[]; + label: string | (() => string); + subMenuOrder: string[]; +}; +export const sidebarRouteConfig = { + components: { + routes: componentRoutes, + label: () => useI18n().t('components.component'), + subMenuOrder: ['nav', 'operator', 'input', 'container', 'display', 'feedback'], + }, +} satisfies Record; + +export type SidebarNameT = keyof typeof sidebarRouteConfig; diff --git a/packages/docs/src/stores/sidebar.ts b/packages/docs/src/stores/sidebar.ts new file mode 100644 index 0000000000000000000000000000000000000000..b5ae6dd13782a54293efb5a4da4378b520bb2cb7 --- /dev/null +++ b/packages/docs/src/stores/sidebar.ts @@ -0,0 +1,115 @@ +import { defineStore } from 'pinia'; +import { computed, ref, watch } from 'vue'; +import { useRouter } from 'vue-router'; +import { sidebarRouteConfig, type SidebarNameT } from '@/router'; +import { useI18n } from '@opensig/opendesign'; +export type NavItem = { + value: string; + label: string; + children?: NavItem[]; +}; +/** 从NavItem数组中找到一个最深的且最近的节点,该节点就是菜单中第一个可跳转的节点 */ +const getDeepestNearestNavItem = (navList: NavItem[]): NavItem | null => { + for (const item of navList) { + if (item.children?.length) { + const deepest = getDeepestNearestNavItem(item.children); + if (deepest) { + return deepest; + } + } else { + return item; + } + } + return null; +}; +/** 是否有名为sidebarName的侧边栏 */ +const hasTheSidebar = (sidebarName: string): sidebarName is SidebarNameT => { + return Object.prototype.hasOwnProperty.call(sidebarRouteConfig, sidebarName); +}; +/** 对 NavItem 进行深度排序 */ +const deepSort = (children: NavItem[]) => { + children.sort((a, b) => a.label.localeCompare(b.label)); + children.forEach((child) => child.children && deepSort(child.children)); +}; +/** 对 NavItem 进行深度排序:第一层按照 firstLevelOrder 排序,其余深度按照标题字符串排序 */ +const sortNavItem = (navList: NavItem[], firstLevelOrder: string[] = []) => { + const firstLevelOrderMap = firstLevelOrder.reduce( + (acc, cur, index) => { + acc[cur] = index; + return acc; + }, + {} as Record, + ); + navList.sort((a, b) => { + const aOrder = firstLevelOrderMap[a.value] ?? 0; + const bOrder = firstLevelOrderMap[b.value] ?? 0; + return aOrder - bOrder; + }); + navList.forEach((item) => item.children && deepSort(item.children)); + return navList; +}; +const getNavList = (sidebarName: string, lang: string, t: (key: string) => string) => { + if (!sidebarName || !hasTheSidebar(sidebarName)) { + return []; + } + const { routes, subMenuOrder } = sidebarRouteConfig[sidebarName]; + const _navList: NavItem[] = []; + const navItemRecord: Record = {}; + for (const item of routes) { + const meta = item.meta; + if (meta.lang === lang) { + const kind = meta.kind; + let subMenu = navItemRecord[kind]; + if (!subMenu) { + subMenu = { value: kind, label: t(`menu.${kind}`), children: [] }; + navItemRecord[kind] = subMenu; + _navList.push(subMenu); + } + subMenu.children?.push({ + value: item.path, + label: meta.sidebar, + }); + } + } + sortNavItem(_navList, subMenuOrder); + return _navList; +}; +const normalizeSidebarName = (sidebarName: any) => { + if (typeof sidebarName === 'string' && hasTheSidebar(sidebarName)) { + return sidebarName; + } + return ''; +}; +export const useSidebarStore = defineStore('sidebar', () => { + const router = useRouter(); + const { locale, t } = useI18n(); + const sidebarName = ref(''); + watch( + () => router.currentRoute.value.meta.sidebarName, + (newVal) => { + sidebarName.value = normalizeSidebarName(newVal); + }, + { immediate: true }, + ); + const navList = computed(() => getNavList(sidebarName.value, locale.value, t)); + const hasData = computed(() => sidebarName.value && navList.value.length); + const changeSidebar = (_sidebarName: SidebarNameT | '') => { + if (hasTheSidebar(_sidebarName)) { + sidebarName.value = _sidebarName; + const dfNavItem = getDeepestNearestNavItem(navList.value); + if (dfNavItem) { + router.push(dfNavItem.value); + } else { + sidebarName.value = _sidebarName; + } + } else { + sidebarName.value = ''; + } + }; + return { + sidebarName: computed(() => sidebarName.value), + navList, + hasData, + changeSidebar, + }; +}); diff --git a/packages/docs/src/stores/theme.ts b/packages/docs/src/stores/theme.ts new file mode 100644 index 0000000000000000000000000000000000000000..50b1690d931b369b751721bc393d800652906cb4 --- /dev/null +++ b/packages/docs/src/stores/theme.ts @@ -0,0 +1,128 @@ +import { computed, ref } from 'vue'; +import { useRouter } from 'vue-router'; +import { defineStore } from 'pinia'; +import { usePrefetch } from '@/utils/optimize'; +import openDesignSkin from '@opensig/opendesign/theme/opendesign/index.scss?url'; +import kunpengSkin from '@opensig/opendesign/theme/kunpeng/index.scss?url'; +import ascendSkin from '@opensig/opendesign/theme/ascend/index.scss?url'; +import eulerSkin from '@opensig/opendesign/theme/openeuler/index.scss?url'; + +export const skin = [ + { + value: '', + name: 'OpenDesign', + }, + { + value: 'e', + name: 'OpenEuler', + }, + { + value: 'a', + name: 'Ascend', + }, + { + value: 'k', + name: 'Kunpeng', + }, +] as const; +export type SkinT = (typeof skin)[number]; +const skinMap = new Map(skin.map((item) => [item.value, item.name])); +export const colors = ['light', 'dark'] as const; +const colorSet = new Set(colors); +export type ColorT = (typeof colors)[number]; +export const linkConfig: Record = { + e: eulerSkin, + k: kunpengSkin, + a: ascendSkin, +}; +usePrefetch([ + { url: openDesignSkin, as: 'style' }, + { url: kunpengSkin, as: 'style' }, + { url: ascendSkin, as: 'style' }, + { url: eulerSkin, as: 'style' }, +]); +const LINK_DOM_MARK = '__docs_theme_link_dom__'; +export const QUERY_SKIN = '__skin'; +export const QUERY_COLOR = '__color'; +export const DEFAULT_COLOR = 'light'; +export const DEFAULT_SKIN_VALUE = ''; +export const DEFAULT_SKIN_HREF = openDesignSkin; + +export const normalizeSkin = (skinValue: any): SkinT['value'] => { + if (skinMap.has(skinValue)) { + return skinValue; + } + return DEFAULT_SKIN_VALUE; +}; +export const normalizeColor = (colorValue: any): ColorT => { + if (colorSet.has(colorValue)) { + return colorValue; + } + return DEFAULT_COLOR; +}; +export const parseTheme = (theme: string) => { + const sc = theme.split('.'); + let skinValue = ''; + let colorValue = ''; + if (sc.length === 2) { + skinValue = sc[0]; + colorValue = sc[1]; + } else { + colorValue = sc[0]; + } + return { + skin: normalizeSkin(skinValue), + color: normalizeColor(colorValue), + }; +}; +export const useThemeStore = defineStore('theme', () => { + /** 皮肤 */ + const skinValue = ref(''); + /** 皮肤名称 */ + const skinName = computed(() => skinMap.get(skinValue.value) || skinMap.get(DEFAULT_SKIN_VALUE)); + /** 颜色 */ + const color = ref(DEFAULT_COLOR); + /** 主题 */ + const theme = computed(() => `${skinValue.value ? `${skinValue.value}.` : ''}${color.value}`); + const router = useRouter(); + + let oldSkinValue: SkinT['value'] | undefined = undefined; + const setSkin = (newVal: SkinT['value']) => { + const styleHref = linkConfig[newVal] || DEFAULT_SKIN_HREF; + const linkDom = document.createElement('link'); + linkDom.rel = 'stylesheet'; + linkDom.href = styleHref; + linkDom.dataset.skinMark = `${LINK_DOM_MARK}${newVal}`; + document.head.insertBefore(linkDom, document.head.firstElementChild); + linkDom.onload = async () => { + skinValue.value = newVal; + document.documentElement.dataset.oTheme = theme.value; + if (oldSkinValue !== undefined) { + document.head.querySelector(`link[data-skin-mark="${LINK_DOM_MARK}${oldSkinValue}"]`)?.remove(); + await router.isReady(); + const route = router.currentRoute.value; + if (route.matched.length) { + router.replace({ ...route, query: { ...route.query, [QUERY_SKIN]: newVal === DEFAULT_SKIN_VALUE ? undefined : newVal } }); + } + } + oldSkinValue = newVal; + }; + }; + const setColor = async (newVal: ColorT) => { + color.value = newVal; + document.documentElement.dataset.oTheme = theme.value; + await router.isReady(); + const route = router.currentRoute.value; + if (route.matched.length) { + router.replace({ ...route, query: { ...route.query, [QUERY_COLOR]: newVal === DEFAULT_COLOR ? undefined : newVal } }); + } + }; + return { + skinValue: computed(() => skinValue.value), + skinName, + color: computed(() => color.value), + theme, + setSkin, + setColor, + }; +}); diff --git a/packages/docs/src/utils/code.ts b/packages/docs/src/utils/code.ts new file mode 100644 index 0000000000000000000000000000000000000000..7f586c0a03bedb5ef3068743ba8d67b5a603b6b1 --- /dev/null +++ b/packages/docs/src/utils/code.ts @@ -0,0 +1,56 @@ +import { type Component } from 'vue'; +import { type CompilerOptions, type CodegenResult } from '@vue/compiler-dom'; +import { createHighlighter } from '../../plugins/markdown/highlight'; + +let Vue: any; +let highlighter: undefined | ((code: string, lang: string) => string); +let compile: undefined | ((template: string, options?: CompilerOptions) => CodegenResult); +let prettierModule: any; +let htmlPlugin: any; +let babelPlugin: any; +let postPlugin: any; +let tsPlugin: any; +let esTreePlugin: any; +/** + * 将模板编译为组件 + * @param template 模板字符串 + * @param ctx 在模板中使用的数据 + * @param options + * @returns 组件 + */ +export async function compileComponent(template: string, ctx: any = {}, options: Omit = {}): Promise { + Vue = Vue ?? (await import('vue')); + compile = compile ?? (await import('@vue/compiler-dom').then((m) => m.compile)); + const { code } = compile!(template, { + ...options, + mode: 'function', + }); + const component = new Function('Vue', 'ctx', code)(Vue, ctx); + return component; +} + +export async function highlight(code: string, lang: string) { + if (!highlighter) { + highlighter = await createHighlighter(); + } + return highlighter(code, lang); +} + +export async function prettier(code: string, parser: string): Promise { + if (!prettierModule) { + [prettierModule, htmlPlugin, babelPlugin, postPlugin, tsPlugin, esTreePlugin] = await Promise.all([ + import('prettier'), + import('prettier/plugins/html'), + import('prettier/plugins/babel'), + import('prettier/plugins/postcss'), + import('prettier/plugins/typescript'), + import('prettier/plugins/estree'), + ]); + } + return prettierModule.format(code, { + parser, + plugins: [htmlPlugin, esTreePlugin, babelPlugin, postPlugin, tsPlugin], + singleQuote: true, + printWidth: 120, + }); +} diff --git a/packages/docs/src/utils/getHeads.ts b/packages/docs/src/utils/getHeads.ts new file mode 100644 index 0000000000000000000000000000000000000000..61e26fb7b40012c48b5d21cba407a8b62685cf44 --- /dev/null +++ b/packages/docs/src/utils/getHeads.ts @@ -0,0 +1,65 @@ +/** + * 将h标签的标题转换为规范的id(该id会作为h标签的id以及a便签的href) + * @param str 待转换的标题 + * @returns id + */ +function slugify(str: string) { + return str + // 将驼峰转为中横线 + .replace(/(?<=[a-z])([A-Z])|(?<=[A-Z])([A-Z][a-z])/g, '-$&') + .toLowerCase() + // 删除标点符号(含中文标点)及表情符号前后的空格 + .replace(/\s*([\p{Punctuation}\p{Symbol}])\s*/gu, '$1') + // 将空白字符转为中横线 + .replace(/\s+/g, '-') + // 合并多个中横线 + .replace(/-+/g, '-') + // 移除首尾中横线 + .replace(/(^-|-$)/g, ''); + // 不应对 hash 进行编码 +} +/** + * 获取h标签 + * @param el h标签的父元素dom,用来限定h标签查找范围 + * @param minLevel 查找的最小级别,默认为2,即只查找h2到h6的标题 + * @returns 查找结果,格式为[{title: string, level: number, id: string}] + */ +export function getHeads(el: HTMLElement, _minLevel = 2) { + const headerId: Record = {}; + const heads: Array<{ title: string; level: number; id: string }> = []; + let levels = ''; + const minLevel = Math.max(Math.floor(_minLevel), 1); + if (minLevel > 6) { + return heads; + } + for (let i = minLevel; i <= 6; i++) { + levels += `h${i}, `; + } + const headDoms = el.querySelectorAll(levels.slice(0, -2)); + headDoms.forEach((dom) => { + const title = dom.textContent; + if (!title) { + return; + } + const level = parseInt(dom.tagName.slice(1)); + let id = ''; + if (dom.id) { + id = dom.id; + } else { + id = slugify(title); + } + // 判断是否有重名id,如果有则加上数字编号;该id会作为锚点的href + if (headerId[id]) { + id = `${id}-${headerId[id]++}`; + } else { + headerId[id] = 1; + } + dom.id = id; + heads.push({ + level, + id, + title, + }); + }); + return heads; +} diff --git a/packages/docs/src/utils/named.ts b/packages/docs/src/utils/named.ts new file mode 100644 index 0000000000000000000000000000000000000000..54e82ff07d921f5024a1554f4072da4e3374c7bb --- /dev/null +++ b/packages/docs/src/utils/named.ts @@ -0,0 +1,8 @@ +const hyphenateRE = /\B([A-Z])/g; +export function hyphenate(str: string) { + return str.replace(hyphenateRE, '-$1').toLowerCase(); +} +const camelizeRE = /-(\w)/g; +export function camelize(str: string) { + return str.replace(camelizeRE, (_, c) => (c ? c.toUpperCase() : '')); +} diff --git a/packages/docs/src/utils/optimize.ts b/packages/docs/src/utils/optimize.ts new file mode 100644 index 0000000000000000000000000000000000000000..2438fb501180469b356aea360588ca02201f79d4 --- /dev/null +++ b/packages/docs/src/utils/optimize.ts @@ -0,0 +1,39 @@ +import { getCurrentInstance, onUnmounted } from 'vue'; + +const links = new Set(); + +export const usePrefetch = (urls: Array<{ url: string; as: string; type?: string }>) => { + if (!urls?.length) return; + const addedLinks: [string, HTMLLinkElement][] = []; + + urls.forEach((item) => { + // 如果已经存在,跳过添加 + if (links.has(item.url)) { + if (process.env.NODE_ENV === 'development') { + console.warn(`[prefetch] ${item.url} has been added.`); + } + return; + } + + const link = document.createElement('link'); + link.rel = 'prefetch'; + link.as = item.as; + if (item.type) { + link.type = item.type; + } + link.href = item.url; + + document.head.appendChild(link); + links.add(item.url); + addedLinks.push([item.url, link]); + }); + + if (getCurrentInstance() && addedLinks.length) { + onUnmounted(() => { + addedLinks.forEach(([url, link]) => { + link.remove(); + links.delete(url); + }); + }); + } +}; diff --git a/packages/docs/src/utils/useScreen.ts b/packages/docs/src/utils/useScreen.ts new file mode 100644 index 0000000000000000000000000000000000000000..8400c53411f832348b30cd8a436c74e0edf79c61 --- /dev/null +++ b/packages/docs/src/utils/useScreen.ts @@ -0,0 +1,27 @@ +import { computed, onMounted, onUnmounted, ref } from 'vue'; + +const DEFAULT_SCREEN_SIZE = 0; +const isClient = typeof window !== 'undefined'; +export const useScreen = () => { + const width = ref(isClient ? window.innerWidth : DEFAULT_SCREEN_SIZE); + + const onResize = () => { + width.value = window.innerWidth; + }; + + onMounted(() => { + window.addEventListener('resize', onResize); + }); + + onUnmounted(() => { + window.removeEventListener('resize', onResize); + }); + + return { + lePad: computed(() => width.value <= 1200), + lePadV: computed(() => width.value <= 840), + isPadV: computed(() => width.value <= 840 && width.value > 600), + isPhone: computed(() => width.value <= 600), + width, + }; +}; diff --git a/packages/portal-ak/src/vite-env.d.ts b/packages/docs/src/vite-env.d.ts similarity index 46% rename from packages/portal-ak/src/vite-env.d.ts rename to packages/docs/src/vite-env.d.ts index af36df349a654c7c39061abbf656a6e5b5b48f8c..0a9d2e4926d3c749bd53660bde2271f8aa850d8c 100644 --- a/packages/portal-ak/src/vite-env.d.ts +++ b/packages/docs/src/vite-env.d.ts @@ -5,3 +5,12 @@ declare module '*.vue' { const component: DefineComponent<{}, {}, any>; export default component; } + +declare module '*.md' { + import type { DefineComponent } from 'vue'; + const component: DefineComponent<{}, {}, any>; + export default component; +} + +declare const __DEV__: boolean; +declare const __PROD__: boolean; diff --git a/packages/docs/switch.md b/packages/docs/switch.md deleted file mode 100644 index 4d13f3ebb1ca4a7ef37761e70a81e3255fec2353..0000000000000000000000000000000000000000 --- a/packages/docs/switch.md +++ /dev/null @@ -1,30 +0,0 @@ -# Switch 开关 - -## props - -| name | type | 默认值 | 说明 | -| :------------------ | :-------------------------------------------- | :------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | -| modelValue(v-model) | string \| number \| boolean | - | 可选,双向绑定值 | -| checkedValue | string \| number \| boolean | true | 可选,选中状态对应值 | -| uncheckedValue | string \| number \| boolean | false | 可选,未选中状态对应值 | -| defaultChecked | boolean | false | 非受控状态时,默认是否选中 | -| size | 'large' \| 'normal' \| 'small' | 'normal' | 可选,开关尺寸 | -| round | string | - | 可选,开关圆角 | -| disabled | boolean | false | 可选,是否禁用 | -| loading | boolean | false | 可选,是否加载中 | -| beforeChange | (val: boolean): Promise\ \| boolean | - | 可选,状态改变前的钩子函数,返回 true 或者返回 promise 且 resolve(true)则继续切换,返回 false 或者返回 promise 且被 reject 或 resolve(false)则阻止切换, | - -## event - -| name | 参数 | 说明 | -| :----- | :------------------------------------------ | :------------- | -| change | val: string \| number \| boolean, ev: Event | 状态切换后触发 | - -## expose - -## slot - -| name | 说明 | -| :--- | :--------------------- | -| on | 自定义开启状态文字内容 | -| off | 自定义关闭状态文字内容 | diff --git a/packages/docs/tab.md b/packages/docs/tab.md deleted file mode 100644 index 9bb357c6512a37391a02d58da555eb3b7e55b3ff..0000000000000000000000000000000000000000 --- a/packages/docs/tab.md +++ /dev/null @@ -1,58 +0,0 @@ -# Tab 页签 - -页签组件,支持切换页签显示不同内容 - -## props - -### OTab - -| name | type | 默认值 | 说明 | -| :---------- | :----------------------------- | :-------- | ------------------------------------ | -| modelValue | string \| number(v-model) | '' | 可选,开关状态 | -| lazy | boolean | false | 可选,是否在首次激活标签时再挂载内容 | -| addable | boolean | false | 可选,是否可以添加页签 | -| variant | "solid" \| "outline" \| "text" | 'outline' | 可选,按钮类型 | -| addInactive | boolean | false | 可选,不激活新添加页签 | -| line | boolean | true | 可选,是否展示 nav 线 | - -### OTabPane - -| name | type | 默认值 | 说明 | -| :------------ | :--------------- | :-------------- | ------------------------------------ | -| value | string \| number | instance.uid | 可选,tab id | -| label | string | undefined | 可选,页签文本 | -| transition | string | o-fade-up-enter | 可选,页签切换时过渡动画 | -| lazy | boolean | false | 可选,是否在首次激活标签时再挂载内容 | -| unmountOnHide | boolean | false | 可选,是否在未激活时卸载页签内容 | -| disabled | boolean | false | 可选,是否禁用选中 | -| closable | boolean | false | 可选,是否可以删除 | - -## event - -### OTab - -| name | 参数 | 说明 | -| :----- | :------------------------------------------------------ | :--------------- | -| change | (value: string \| number, oldValue: string \| number) | 页签切换后触发 | -| change | (value: string \| number, oldValue?: string \| number ) | 页签切换变化触发 | -| delete | (value: string \| number ) | 页签删除后触发 | -| add | (evt: MouseEvent ) | 页签添加后触发 | - -## expose - -## slot - -### OTab - -| name | 说明 | -| :----- | :------- | -| prefix | 前缀插槽 | -| suffix | 后缀插槽 | -| anchor | 高亮插槽 | - -### OTabPane - -| name | 说明 | -| :------ | :----------- | -| nav | 页签头部插槽 | -| default | 页签内容插槽 | diff --git a/packages/docs/table.md b/packages/docs/table.md deleted file mode 100644 index 0f43cc5b3ef3bfd79a208f06951f79ad0d27d38c..0000000000000000000000000000000000000000 --- a/packages/docs/table.md +++ /dev/null @@ -1,9 +0,0 @@ -# Table 表格 - -## props - -## event - -## expose - -## slot diff --git a/packages/docs/tag.md b/packages/docs/tag.md deleted file mode 100644 index cd36edfb8ebf4c0d35e7f892adfa9869a4250369..0000000000000000000000000000000000000000 --- a/packages/docs/tag.md +++ /dev/null @@ -1,28 +0,0 @@ -# Tag 标签 - -## props - -| name | type | 默认值 | 说明 | -| :--------------- | :--------------------------------------------- | :------- | ------------------------------------------------------------- | -| color | 'normal' \| 'success' \| 'warning' \| 'danger' | 'normal' | 可选,标签颜色 | -| variant | 'solid'\|'outline' | 'solid' | 可选,标签类型 | -| size | 'large' \| 'normal' \| 'small' | 'normal' | 可选,标签尺寸 | -| round | string | - | 可选,标签圆角 | -| closable | boolean | false | 可选,是否可关闭 | -| checkable | boolean | false | 可选,是否可选中 | -| checked(v-model) | boolean | false | 可选,是否被选中(仅 checkable 为 true 生效) | -| defaultChecked | boolean | false | 可选,非受控状态时,默认是否选中(仅 checkable 为 true 生效) | - -## event - -| name | 参数 | 说明 | -| :----- | :--------------------------- | :----------- | -| change | val: boolean, ev: MouseEvent | 值改变时触发 | -| close | ev: MouseEvent | 关闭时触发 | - -## slot - -| name | 说明 | -| :------ | :------------- | -| icon | 自定义图标 | -| default | 自定义标签内容 | diff --git a/packages/portal-ak/tsconfig.json b/packages/docs/tsconfig.json similarity index 83% rename from packages/portal-ak/tsconfig.json rename to packages/docs/tsconfig.json index a0ed615c813b868cfc04d7d50f996d69e5ee6c62..abd00da32805aaa6ccc8812cd2bd476780da7677 100644 --- a/packages/portal-ak/tsconfig.json +++ b/packages/docs/tsconfig.json @@ -3,7 +3,7 @@ "target": "ESNext", "useDefineForClassFields": true, "module": "ESNext", - "moduleResolution": "Node", + "moduleResolution": "bundler", "strict": true, "jsx": "preserve", "resolveJsonModule": true, @@ -20,8 +20,11 @@ "@/*": [ "src/*" ], - "@components/*": [ + "@opensig/opendesign/*": [ "../opendesign/src/*" + ], + "@opensig/opendesign": [ + "../opendesign/src/index.ts" ] } }, diff --git a/packages/docs/tsconfig.node.json b/packages/docs/tsconfig.node.json new file mode 100644 index 0000000000000000000000000000000000000000..40acac9c6b00e9cdd28652ec3174ff3a94e01c7e --- /dev/null +++ b/packages/docs/tsconfig.node.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "composite": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true, + "target": "esnext" + }, + "include": [ + "vite.config.ts", + "plugins/**/*.ts", + "scripts/**/*.ts", + "helper/**/*.ts" + ] +} \ No newline at end of file diff --git a/packages/docs/vite.config.ts b/packages/docs/vite.config.ts new file mode 100644 index 0000000000000000000000000000000000000000..a3ff014cf2b7f683869c0489cfe6752d6b99106e --- /dev/null +++ b/packages/docs/vite.config.ts @@ -0,0 +1,48 @@ +import path from 'path'; +import { defineConfig } from 'vite'; +import vue from '@vitejs/plugin-vue'; +import { plugin as markdownPlugin } from './plugins/markdown/vueMdTranslate'; +import Inspect from 'vite-plugin-inspect'; + +import { injectDemoAndApi } from './plugins/injectDemoAndApi'; +import { injectDemoSource } from './plugins/injectDemoSource'; +import { injectDemoDocs } from './plugins/injectDemoDocs'; +import generateComponentRouter from './plugins/generateComponentRouter'; + +// https://vitejs.dev/config/ +export default defineConfig({ + build: { + target: ['chrome74'], + outDir: './dist', + }, + plugins: [ + vue({ include: [/\.vue$/, /\.md$/] }), + injectDemoAndApi(), + injectDemoSource(), + injectDemoDocs(), + markdownPlugin, + generateComponentRouter(), + Inspect(), + ], + resolve: { + alias: { + '@/': `${path.resolve(__dirname, './src')}/`, + '@assets': path.resolve(__dirname, './src/assets'), + '@opensig/opendesign': path.resolve(__dirname, '../opendesign/src'), + }, + }, + server: { + port: 3300, + }, + define: { + __DEV__: JSON.stringify(process.env.NODE_ENV === 'development'), + __PROD__: JSON.stringify(process.env.NODE_ENV === 'production'), + }, + css: { + preprocessorOptions: { + scss: { + additionalData: '@use "@opensig/opendesign/_styles/mixin.scss" as *;\n', + }, + }, + }, +}); diff --git a/packages/opendesign/.gitignore b/packages/opendesign/.gitignore index da20fbc80ad491a5a6a7b7a1ff89a0a3fc400797..ffab77fac1053156b08e3b53c11fb0d11caa5705 100644 --- a/packages/opendesign/.gitignore +++ b/packages/opendesign/.gitignore @@ -24,4 +24,6 @@ dist-ssr *.sw? es -lib \ No newline at end of file +lib +code-snippets +*api.*.md diff --git a/packages/opendesign/README.md b/packages/opendesign/README.md index b6adf0304ef99a3e0539fa367c8cac32584a6a92..cac4111197e5c3d14b6ac7b56514368d27e8090b 100644 --- a/packages/opendesign/README.md +++ b/packages/opendesign/README.md @@ -1,3 +1,56 @@ -# opendesign components +

+

opendesign

+

一个 Vue 3 组件库

+

皮肤可定制,使用 TypeScript

-- vue3 components for opendesign + +## 特性 + + +### 皮肤可定制 + +提供组件变量体系,可以快速定义一套新皮肤 + +### 使用 TypeScript + +opendesign 全量使用 TypeScript 编写,和你的 TypeScript 项目无缝衔接。 + + +## 安装 + +### npm + +使用 npm/pnpm 安装。 + +```bash +# npm +npm i @opensig/opendesign + +#pnpm +pnpm add @opensig/opendesign + +``` + + +## 使用 + +### 引入样式文件 +``` +import '@opensig/opendesign/es/index.scss' +``` + +### 使用组件 +``` + + +``` + + +## 许可 + +opendesign 使用 [MIT license](https://opensource.org/licenses/MIT) 许可证书。 \ No newline at end of file diff --git a/packages/opendesign/icons/svgs/fill/one-to-one.svg b/packages/opendesign/icons/svgs/fill/one-to-one.svg new file mode 100644 index 0000000000000000000000000000000000000000..daef3af7bbf6f498b91ee1bda0c3434222db6fc1 --- /dev/null +++ b/packages/opendesign/icons/svgs/fill/one-to-one.svg @@ -0,0 +1,8 @@ + + one-to-one + + + + + + diff --git a/packages/opendesign/icons/svgs/fill/zoom-in.svg b/packages/opendesign/icons/svgs/fill/zoom-in.svg new file mode 100644 index 0000000000000000000000000000000000000000..bc689d7af7f5824241d7c7f7047885f167d5a249 --- /dev/null +++ b/packages/opendesign/icons/svgs/fill/zoom-in.svg @@ -0,0 +1,5 @@ + +zoom-in + + + diff --git a/packages/opendesign/icons/svgs/fill/zoom-out.svg b/packages/opendesign/icons/svgs/fill/zoom-out.svg new file mode 100644 index 0000000000000000000000000000000000000000..42dd7f209dbaa1b44ef831332f8e94dc52db1630 --- /dev/null +++ b/packages/opendesign/icons/svgs/fill/zoom-out.svg @@ -0,0 +1,5 @@ + +zoom-out + + + diff --git a/packages/opendesign/package.json b/packages/opendesign/package.json index 0a00beb49cfb1fcceed7a490c3d7813ae0945c46..1e686bb9a9f272dd576bf1f7aadd5c6049853b0f 100644 --- a/packages/opendesign/package.json +++ b/packages/opendesign/package.json @@ -1,6 +1,6 @@ { "name": "@opensig/opendesign", - "version": "0.0.78", + "version": "1.0.2", "license": "MIT", "main": "lib/index.js", "module": "es/index.mjs", @@ -17,24 +17,23 @@ "vue3 components" ], "scripts": { - "gen:token": "open-scripts gen:token --config ./tokens/token.config.ts", + "gen:token": "open-scripts gen:token --config ./src/theme/opendesign/token.config.ts && open-scripts gen:token --config ./src/theme/ascend/token.config.ts && open-scripts gen:token --config ./src/theme/kunpeng/token.config.ts && open-scripts gen:token --config ./src/theme/openeuler/token.config.ts", "gen:icon": "open-scripts gen:icon --config ./icons/icon.config.ts", "clean:svg": "open-scripts clean:svg --config ./icons/cleansvg.config.ts", "build:component": "open-scripts build:component", "build:style": "open-scripts build:style", "build": "pnpm gen:icon && pnpm build:component && pnpm build:style", "lint": "eslint ./src", - "type-check": "vue-tsc --build" + "type-check": "vue-tsc --build", + "release": "pnpm publish --registry https://registry.npmjs.org" }, "peerDependencies": { "vue": "^3.3.0" }, "devDependencies": { - "@opensig/open-scripts": "0.0.23", - "typescript": "~5.8.2", - "vue-tsc": "2.2.8" - }, - "dependencies": { - "date-fns": "^2.30.0" + "@opensig/open-scripts": "workspace:^", + "typescript": "catalog:typescript", + "vue-tsc": "catalog:build", + "vite": "catalog:build" } } diff --git a/packages/opendesign/src/_components/in-box/InBox.vue b/packages/opendesign/src/_components/in-box/InBox.vue index e89089d9267d4fe9fc9a7dddb5518ba70eaea3c3..5106a0ac143f5e4843c8bf5b8eb063cb89046ceb 100644 --- a/packages/opendesign/src/_components/in-box/InBox.vue +++ b/packages/opendesign/src/_components/in-box/InBox.vue @@ -32,7 +32,6 @@ const round = getRoundClass(props, '_box'); 'has-prepend': $slots.prepend, 'has-append': $slots.append, }, - round.class.value, ]" > diff --git a/packages/opendesign/src/_components/in-box/style/index.scss b/packages/opendesign/src/_components/in-box/style/index.scss index 6c43b4f60ecc08d603d01dcbdf0f8f4ae251fb1e..38241341183a577cbd07f9d6804a32a2dd1d7583 100644 --- a/packages/opendesign/src/_components/in-box/style/index.scss +++ b/packages/opendesign/src/_components/in-box/style/index.scss @@ -20,7 +20,6 @@ background-color: var(--_box-bg-color); transition: all var(--o-duration-s) var(--o-easing-standard); - cursor: pointer; @include hover { border-color: var(--_box-bd-color-hover); background-color: var(--_box-bg-color-hover); diff --git a/packages/opendesign/src/_components/in-input/InInput.vue b/packages/opendesign/src/_components/in-input/InInput.vue index 61ccfc544dfa3348ede76c5a1ca162a9f998d4b7..24b783756e08ba1aef9e8d33f062014234a4a926 100644 --- a/packages/opendesign/src/_components/in-input/InInput.vue +++ b/packages/opendesign/src/_components/in-input/InInput.vue @@ -5,6 +5,7 @@ import { IconClose, IconEyeOn, IconEyeOff } from '../../_utils/icons'; import { useInput, type UseInputEmitsT } from '../../_headless/use-input'; import { useInputPassword } from '../../_headless/use-input-password'; import { useI18n } from '../../locale'; +import { isUndefined } from '../../_utils/is'; const props = defineProps(inInputProps); const slots = defineSlots<{ @@ -51,7 +52,7 @@ const { calculateLength: props.getLength, }); -const { showPassword, onEyeMouseDown, onEyeMouseUp, onEyeClick } = useInputPassword({ +const { showPassword, onEyeMouseDown, onEyeClick } = useInputPassword({ type, disabled, showPasswordEvent: props.showPasswordEvent, @@ -61,7 +62,15 @@ const { showPassword, onEyeMouseDown, onEyeMouseUp, onEyeClick } = useInputPassw const inputType = ref(props.type); const togglePassword = (visible?: boolean) => { - inputType.value = visible ? 'text' : 'password'; + if (isUndefined(visible)) { + if (inputType.value === 'text') { + inputType.value = 'password'; + } else { + inputType.value = 'text'; + } + } else { + inputType.value = visible ? 'text' : 'password'; + } }; watchEffect(() => { @@ -75,6 +84,10 @@ const focus = () => { inputEl.value?.focus(); }; +const blur = () => { + inputEl.value?.blur(); +}; + /** * 自适应宽度 */ @@ -89,6 +102,7 @@ const mirrorValue = computed(() => { defineExpose({ inputEl, focus, + blur, clear, togglePassword, }); @@ -139,12 +153,9 @@ defineExpose({
diff --git a/packages/opendesign/src/_components/in-input/__demo__/TheIndex.vue b/packages/opendesign/src/_components/in-input/__demo__/TheIndex.vue index f2ccabfdf782750310f3a070ed72b657c0140952..9e058221aea850e881f94ab885a3074b32b62cd7 100644 --- a/packages/opendesign/src/_components/in-input/__demo__/TheIndex.vue +++ b/packages/opendesign/src/_components/in-input/__demo__/TheIndex.vue @@ -2,7 +2,8 @@ import '../style'; import InInput from '../InInput.vue'; import { ref } from 'vue'; -const inputVal = ref('124567890'); + +const inputVal = ref('124567123'); const printEvent = (evt: string, v?: string) => { console.log(`[${evt}]`, v ?? '', 'inputVal:', inputVal.value); @@ -10,7 +11,7 @@ const printEvent = (evt: string, v?: string) => { const disabled = ref(false); const maxLength = ref(6); -const minLength = ref(4); +const minLength = ref(2); const toggle = () => { disabled.value = !disabled.value; maxLength.value = 5; @@ -28,6 +29,7 @@ const validate = (value: string): boolean => { const onUpdate = (val: string) => { inputVal.value = val; + console.log('onUpdate', val); }; const format = (val: string) => { @@ -36,13 +38,18 @@ const format = (val: string) => { const valueOnInvalidChange = (currentValue: string, lastValid: string) => { console.log('valueOnInvalidChange:', currentValue, lastValid); - return lastValid; + return lastValid || currentValue; }; const onChange = (currentValue: string, lastValue: string) => { console.log('change:', currentValue, lastValue); inputVal.value = currentValue; }; + +const count = ref(1); +window.setInterval(() => { + count.value++; +}, 1000); \ No newline at end of file diff --git a/packages/opendesign/src/anchor/__docs__/__case__/AnchorUsage.vue b/packages/opendesign/src/anchor/__docs__/__case__/AnchorUsage.vue new file mode 100644 index 0000000000000000000000000000000000000000..9f06bf185d41944b81bba3aa7e6d5cc921a72056 --- /dev/null +++ b/packages/opendesign/src/anchor/__docs__/__case__/AnchorUsage.vue @@ -0,0 +1,107 @@ + + + +### 使用 + +`container` 属性 +- 说明:指定锚点监听的滚动容器(默认值:window) +- 用法:组件会在该容器上监听 `scroll` 事件,以判断当前激活的锚点 +- 示例:`container="#wrap"` 表示监听 id 为 wrap 的容器滚动事件 + +`targetOffset` 属性 +- 说明:目标元素跳转或激活时距离容器顶部的偏移量(默认值:0) +- 示例:`:target-offset="10"` 表示点击锚点时目标元素会滚动到距离容器顶部 10px 的位置;滚动距离小于 10px 时锚点处于激活状态 + +`bounds` 属性 +- 说明:设置锚点激活的判定边界(默认值:5) +- 用法:当需要跳转位置和激活判定边界不一致时可设置 +- 示例:`:bounds="100" :target-offset="10"` 表示目标元素滚动到距离容器顶部 110px 时锚点激活。可实现点击锚点时目标元素滚动到顶部,滚动浏览时需到容器中上部才激活锚点的交互效果 + +`changeHash` 属性 +- 说明:是否在点击锚点时改变浏览器地址栏的 hash 值(默认值:true) +- 用法:如果需要手动控制 URL 跳转,可以设置为 false,并在 `click` 事件中处理跳转逻辑 +- 示例:`:change-hash="false"` 表示点击锚点时不会改变浏览器地址栏的 hash 值 + +锚点嵌套:`OAnchorItem` 可以嵌套使用,形成多级锚点结构 + + + +### Usage + +`container` property +- Description: Specifies the scroll container to be monitored by the anchor (default: window) +- Usage: The component listens for the `scroll` event on this container to determine the currently active anchor +- Example: `container="#wrap"` means listening to the scroll event of the container with id "wrap" + +`targetOffset` property +- Description: The offset from the top of the container when the target element is scrolled or activated (default: 0) +- Example: `:target-offset="10"` means when clicking the anchor, the target element will scroll to 10px from the top of the container; when the scroll distance is less than 10px, the anchor is considered active + +`bounds` property +- Description: Sets the boundary for anchor activation (default: 5) +- Usage: Use when the scroll position and activation boundary need to be different +- Example: `:bounds="100" :target-offset="10"` means the anchor is activated when the target element is 110px from the top of the container. This allows the target element to scroll to the top when clicking the anchor, but requires scrolling further down to activate + +`changeHash` property +- Description: Whether to change the browser's address bar hash value when clicking the anchor (default: true) +- Usage: If you need to manually control URL navigation, set this to false and handle the navigation logic in the `click` event +- Example: `:change-hash="false"` means clicking the anchor will not change the hash value in the browser's address bar + +Nested anchors: `OAnchorItem` can be nested to create multi-level anchor structures + + + + + + + diff --git a/packages/opendesign/src/anchor/__docs__/index.en-US.md b/packages/opendesign/src/anchor/__docs__/index.en-US.md new file mode 100644 index 0000000000000000000000000000000000000000..7120ca5471539eeafca7bccf8275ce34e3ff0a02 --- /dev/null +++ b/packages/opendesign/src/anchor/__docs__/index.en-US.md @@ -0,0 +1,16 @@ +--- +sidebar: OAnchor +kind: nav +--- + +# Anchor + +## Demo + + + + +## Api + + + diff --git a/packages/opendesign/src/anchor/__docs__/index.zh-CN.md b/packages/opendesign/src/anchor/__docs__/index.zh-CN.md new file mode 100644 index 0000000000000000000000000000000000000000..ea1b4ae975d5eecf5f008f0eec42980dfca4d85c --- /dev/null +++ b/packages/opendesign/src/anchor/__docs__/index.zh-CN.md @@ -0,0 +1,16 @@ +--- +sidebar: OAnchor 锚点 +kind: nav +--- + +# 锚点 + +## 示例 + + + + +## Api + + + diff --git a/packages/opendesign/src/anchor/style/index.scss b/packages/opendesign/src/anchor/style/index.scss index 504d59c44eea7243e3faa4e454b7675ea8b98768..8bb47c5840ef5591b0176e882570a9878c439b64 100644 --- a/packages/opendesign/src/anchor/style/index.scss +++ b/packages/opendesign/src/anchor/style/index.scss @@ -1,2 +1,3 @@ @use './style.scss' as *; +@use './var.scss' as *; @use './media.scss' as *; diff --git a/packages/opendesign/src/anchor/style/media.scss b/packages/opendesign/src/anchor/style/media.scss index f415089a72379f537e904f06a5668e095fa1b0f7..1f1e6278f24e4ea00bff8fabeb50f40fd97d2d89 100644 --- a/packages/opendesign/src/anchor/style/media.scss +++ b/packages/opendesign/src/anchor/style/media.scss @@ -1,8 +1,9 @@ @use '../../_styles/mixin.scss' as *; -@include respond-to('<=laptop') { +@include respond-to('<=pc_s') { .o-anchor-item { --anchor-item-link-text-size: var(--o-font_size-tip1); --anchor-item-link-text-height: var(--o-line_height-tip1); + --anchor-item-text-indent: 8px; } } diff --git a/packages/opendesign/src/anchor/style/style.scss b/packages/opendesign/src/anchor/style/style.scss index daf4883e26556eaf42cef987e415e9acac5fb7c8..5a864bfd83584661c2dcb76fe2b26ea87fffd8db 100644 --- a/packages/opendesign/src/anchor/style/style.scss +++ b/packages/opendesign/src/anchor/style/style.scss @@ -1,5 +1,4 @@ @use '../../_styles/mixin.scss' as *; -@use './var.scss'; .o-anchor { position: relative; @@ -48,7 +47,11 @@ font-size: var(--anchor-item-link-text-size); line-height: var(--anchor-item-link-text-height); background-color: var(--anchor-item-link-bg-color); - padding: var(--anchor-item-link-padding); + padding: + var(--anchor-item-link-padding-v) + var(--anchor-item-link-padding-h) + var(--anchor-item-link-padding-v) + calc(var(--anchor-item-text-indent) * var(--anchor-item-depth) + var(--anchor-item-link-padding-h)); border-radius: var(--anchor-item-link-radius); transition: background-color var(--o-duration-s) var(--o-easing-standard); diff --git a/packages/opendesign/src/anchor/style/theme-ascend.index.ts b/packages/opendesign/src/anchor/style/theme-ascend.index.ts new file mode 100644 index 0000000000000000000000000000000000000000..b0049a088358468a6b9185cfea3d656b4f1faaf9 --- /dev/null +++ b/packages/opendesign/src/anchor/style/theme-ascend.index.ts @@ -0,0 +1,4 @@ +import '../../_styles'; +import '../../select/style/theme-ascend.index'; +import './index.scss'; +import './theme-ascend.scss'; diff --git a/packages/opendesign/src/anchor/style/theme-ascend.scss b/packages/opendesign/src/anchor/style/theme-ascend.scss new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/packages/opendesign/src/anchor/style/theme-kunpeng.index.ts b/packages/opendesign/src/anchor/style/theme-kunpeng.index.ts new file mode 100644 index 0000000000000000000000000000000000000000..08f1ffe5704754a7322de7adff8f64b9f43f7379 --- /dev/null +++ b/packages/opendesign/src/anchor/style/theme-kunpeng.index.ts @@ -0,0 +1,4 @@ +import '../../_styles'; +import '../../select/style/theme-kunpeng.index'; +import './index.scss'; +import './theme-kunpeng.scss'; diff --git a/packages/opendesign/src/anchor/style/theme-kunpeng.scss b/packages/opendesign/src/anchor/style/theme-kunpeng.scss new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/packages/opendesign/src/anchor/style/theme-openeuler.index.ts b/packages/opendesign/src/anchor/style/theme-openeuler.index.ts new file mode 100644 index 0000000000000000000000000000000000000000..93ba6509e35367f9c7ca12d2142dcb65d50c0b55 --- /dev/null +++ b/packages/opendesign/src/anchor/style/theme-openeuler.index.ts @@ -0,0 +1,4 @@ +import '../../_styles'; +import '../../select/style/theme-openeuler.index'; +import './index.scss'; +import './theme-openeuler.scss'; diff --git a/packages/opendesign/src/anchor/style/theme-openeuler.scss b/packages/opendesign/src/anchor/style/theme-openeuler.scss new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/packages/opendesign/src/anchor/style/var.scss b/packages/opendesign/src/anchor/style/var.scss index 8d13c1f8acc1be9755c6982d73712f274890cfa3..4c9268cd8f8f7b9b5fa58e6335264e9d71a50bc8 100644 --- a/packages/opendesign/src/anchor/style/var.scss +++ b/packages/opendesign/src/anchor/style/var.scss @@ -20,8 +20,10 @@ --anchor-item-link-bg-color-hover: var(--o-color-control2-light); --anchor-item-link-bg-color-active: var(--o-color-control3-light); - --anchor-item-link-padding: 7px 8px; + --anchor-item-link-padding-h: 8px; + --anchor-item-link-padding-v: 7px; --anchor-item-link-radius: var(--o-radius_control-s); --anchor-item-link-gap: 2px; + --anchor-item-text-indent: 12px; } diff --git a/packages/opendesign/src/anchor/types.ts b/packages/opendesign/src/anchor/types.ts index bc9d688cf6e5356755772f733fc372e9800fe508..063c1ce0ccfe0c8093c3008990c5058caf625efc 100644 --- a/packages/opendesign/src/anchor/types.ts +++ b/packages/opendesign/src/anchor/types.ts @@ -1,17 +1,37 @@ import type { ExtractPropTypes, PropType } from 'vue'; export const anchorProps = { + /** + * @zh-CN 监测容器 + * @en-US Scroll container to monitor + * @default window + */ container: { type: [String, Object] as PropType, }, + /** + * @zh-CN 锚点激活的边界范围 + * @en-US Boundary for anchor activation + * @default 5 + */ bounds: { type: Number, default: 5, }, + /** + * @zh-CN 锚点激活的判定边界 + * @en-US Boundary for anchor activation + * @default 0 + */ targetOffset: { type: Number, default: 0, }, + /** + * @zh-CN 点击锚点时是否改变浏览器地址栏的 hash 值 + * @en-US Whether to change the browser's address bar hash value when clicking the anchor + * @default true + */ changeHash: { type: Boolean, default: true, @@ -19,15 +39,27 @@ export const anchorProps = { }; export const anchorItemProps = { + /** + * @zh-CN 锚点标题 + * @en-US Anchor title + */ title: { type: String, default: '', }, + /** + * @zh-CN 锚点跳转的目标元素(带#前缀) + * @en-US Target element for anchor navigation (with # prefix) + */ href: { type: String, - default: '', required: true, }, + /** + * @zh-CN 锚点跳转方式 + * @en-US Anchor navigation method + * @default '_self' + */ target: { type: String as PropType<'_blank' | '_parent' | '_self' | '_top'>, default: '_self', diff --git a/packages/opendesign/src/badge/OBadge.vue b/packages/opendesign/src/badge/OBadge.vue index f2179454c98f6937e1cf3e0e974406f1cfe5040e..cb096fae652369e07d5b57dd1670bf04b49b09fa 100644 --- a/packages/opendesign/src/badge/OBadge.vue +++ b/packages/opendesign/src/badge/OBadge.vue @@ -18,7 +18,7 @@ const content = computed(() => { const style = computed(() => { const [x, y] = props.offset; - const right = isNumber(x) ? `-${x}px` : `-${x}`; + const right = isNumber(x) ? `${x * -1}px` : `calc(${x} * -1)`; const top = isNumber(y) ? `${y}px` : `${y}`; return { right, diff --git a/packages/opendesign/src/badge/__docs__/__case__/BadgeSlot.vue b/packages/opendesign/src/badge/__docs__/__case__/BadgeSlot.vue new file mode 100644 index 0000000000000000000000000000000000000000..ae7b73f1fdfde77fec62f49ceb2b7188ab74a013 --- /dev/null +++ b/packages/opendesign/src/badge/__docs__/__case__/BadgeSlot.vue @@ -0,0 +1,24 @@ + + + +### 自定义徽标内容 + +可以通过 `content` 插槽自定义徽标内容。如果不提供 `content` 插槽,则显示 `value` 值。 + + + +### Custom Badge Content + +You can customize the badge content using the `content` slot. If the `content` slot is not provided, the `value` will be displayed. + + + diff --git a/packages/opendesign/src/badge/__docs__/__case__/BadgeUsage.vue b/packages/opendesign/src/badge/__docs__/__case__/BadgeUsage.vue new file mode 100644 index 0000000000000000000000000000000000000000..c12258fdf4e2fd641c02b7c63291e4c76d9ed133 --- /dev/null +++ b/packages/opendesign/src/badge/__docs__/__case__/BadgeUsage.vue @@ -0,0 +1,74 @@ + + + +### 使用 + +徽标包含 `primary`、`success`、`warning`、`danger` 四种主题色。 + +徽标 `value` 参数支持数字和文本两种类型的值,当为数字时 `max` 参数会影响值的显示。 + +徽标支持小红点样式,小红点中不会显示徽标内容。 + +徽标可以通过 `offset` 设置偏移位置。 + + + +### Usage + +The badge includes four theme colors: `primary`, `success`, `warning`, and `danger`. + +The `value` parameter of the badge supports both numeric and text values. When it is numeric, the `max` parameter affects how the value is displayed. + +The badge supports a small dot style, where the content is not displayed in the dot. + +The badge can set an offset position using the `offset` property. + + diff --git a/packages/opendesign/src/badge/__docs__/index.en-US.md b/packages/opendesign/src/badge/__docs__/index.en-US.md new file mode 100644 index 0000000000000000000000000000000000000000..fb6d8f0436fd92cd5e524328f1005a62672dfa43 --- /dev/null +++ b/packages/opendesign/src/badge/__docs__/index.en-US.md @@ -0,0 +1,16 @@ +--- +sidebar: OBadge +kind: display +--- + +# OBadge + +## Demo + + + + + +## Api + + diff --git a/packages/opendesign/src/badge/__docs__/index.zh-CN.md b/packages/opendesign/src/badge/__docs__/index.zh-CN.md new file mode 100644 index 0000000000000000000000000000000000000000..d48785f23c2b50614c85ee6649616950a0346914 --- /dev/null +++ b/packages/opendesign/src/badge/__docs__/index.zh-CN.md @@ -0,0 +1,16 @@ +--- +sidebar: OBadge 徽标 +kind: display +--- + +# 徽标 + +## 示例 + + + + + +## Api + + diff --git a/packages/opendesign/src/badge/style/index.scss b/packages/opendesign/src/badge/style/index.scss index 504d59c44eea7243e3faa4e454b7675ea8b98768..8bb47c5840ef5591b0176e882570a9878c439b64 100644 --- a/packages/opendesign/src/badge/style/index.scss +++ b/packages/opendesign/src/badge/style/index.scss @@ -1,2 +1,3 @@ @use './style.scss' as *; +@use './var.scss' as *; @use './media.scss' as *; diff --git a/packages/opendesign/src/badge/style/media.scss b/packages/opendesign/src/badge/style/media.scss index f7a9a3d65833824fa226a36e32e29f0f96bab356..a7233a6864dcd95f560372a2cd408b9e78604429 100644 --- a/packages/opendesign/src/badge/style/media.scss +++ b/packages/opendesign/src/badge/style/media.scss @@ -1,6 +1,6 @@ @use '../../_styles/mixin.scss' as *; -@include respond-to('<=pad') { +@include respond-to('<=pad_v') { .o-badge { --badge-dot-size: 6px; } diff --git a/packages/opendesign/src/badge/style/style.scss b/packages/opendesign/src/badge/style/style.scss index 2ea3242a29cf537082ee823c4a00d66a502dcd41..8b1860d3bc78f23c81ef49d2248b6d110865b3a4 100644 --- a/packages/opendesign/src/badge/style/style.scss +++ b/packages/opendesign/src/badge/style/style.scss @@ -1,5 +1,4 @@ @use '../../_styles/mixin.scss' as *; -@use './var.scss'; .o-badge { position: relative; diff --git a/packages/opendesign/src/badge/style/theme-ascend.index.ts b/packages/opendesign/src/badge/style/theme-ascend.index.ts new file mode 100644 index 0000000000000000000000000000000000000000..5bdacd9c6ee5aca363917db515e154542266f4c2 --- /dev/null +++ b/packages/opendesign/src/badge/style/theme-ascend.index.ts @@ -0,0 +1,3 @@ +import '../../_styles'; +import './index.scss'; +import './theme-ascend.scss'; diff --git a/packages/portal-ak/src/ak/theme/badge.scss b/packages/opendesign/src/badge/style/theme-ascend.scss similarity index 100% rename from packages/portal-ak/src/ak/theme/badge.scss rename to packages/opendesign/src/badge/style/theme-ascend.scss diff --git a/packages/opendesign/src/badge/style/theme-kunpeng.index.ts b/packages/opendesign/src/badge/style/theme-kunpeng.index.ts new file mode 100644 index 0000000000000000000000000000000000000000..c2b4508ff081979f08fbcfda03f045c172570d33 --- /dev/null +++ b/packages/opendesign/src/badge/style/theme-kunpeng.index.ts @@ -0,0 +1,3 @@ +import '../../_styles'; +import './index.scss'; +import './theme-kunpeng.scss'; diff --git a/packages/opendesign/src/badge/style/theme-kunpeng.scss b/packages/opendesign/src/badge/style/theme-kunpeng.scss new file mode 100644 index 0000000000000000000000000000000000000000..1e206277843dfbc06da8fbadbcae3cf2de884daf --- /dev/null +++ b/packages/opendesign/src/badge/style/theme-kunpeng.scss @@ -0,0 +1,3 @@ +.o-badge-primary { + --badge-bg-color: rgb(var(--o-red-huawei)); +} diff --git a/packages/opendesign/src/badge/style/theme-openeuler.index.ts b/packages/opendesign/src/badge/style/theme-openeuler.index.ts new file mode 100644 index 0000000000000000000000000000000000000000..5a4690a05640253920c989242a44e2e2e5b4b720 --- /dev/null +++ b/packages/opendesign/src/badge/style/theme-openeuler.index.ts @@ -0,0 +1,3 @@ +import '../../_styles'; +import './index.scss'; +import './theme-openeuler.scss'; diff --git a/packages/opendesign/src/badge/style/theme-openeuler.scss b/packages/opendesign/src/badge/style/theme-openeuler.scss new file mode 100644 index 0000000000000000000000000000000000000000..da7faa4df3c51390b5871d69755b7fcf615b012a --- /dev/null +++ b/packages/opendesign/src/badge/style/theme-openeuler.scss @@ -0,0 +1,3 @@ +.o-badge-primary { + --badge-bg-color: var(--o-color-danger1); +} diff --git a/packages/opendesign/src/badge/types.ts b/packages/opendesign/src/badge/types.ts index 9ddfe8182f1ede381d8440d942446048db38e47a..7558e73437d4fc085ab819d50945fba9756946bf 100644 --- a/packages/opendesign/src/badge/types.ts +++ b/packages/opendesign/src/badge/types.ts @@ -5,35 +5,43 @@ export type BadgeColorT = (typeof BadgeColorTypes)[number]; export const badgeProps = { /** - * 显示值 + * @zh-CN 徽标内容 + * @en-US Content of the badge */ value: { type: [String, Number], default: '', }, /** - * 最大值,超过最大值显示${max}+(仅当 value 类型为 number 时生效) + * @zh-CN 最大值,超过最大值显示${max}+(仅当 value 类型为 number 时生效) + * @en-US Max value, display ${max}+ if exceeded (only effective when value type is number) + * @default 99 */ max: { type: Number, default: 99, }, /** - * 颜色类型 BadgeColorT + * @zh-CN 徽标颜色 + * @en-US Color of the badge + * @default 'primary' */ color: { type: String as PropType, default: 'primary', }, /** - * 是否显示为小红点 + * @zh-CN 是否显示为小红点 + * @en-US Whether to display as a small red dot + * @default false */ dot: { type: Boolean, default: false, }, /** - * 徽标位置偏移量 + * @zh-CN 徽标位置偏移量 + * @en-US Badge position offset */ offset: { type: Array as PropType>, diff --git a/packages/opendesign/src/breadcrumb/__docs__/__case__/BcUsage.vue b/packages/opendesign/src/breadcrumb/__docs__/__case__/BcUsage.vue new file mode 100644 index 0000000000000000000000000000000000000000..3d2cf8471609e135553884713ed19c9de74c26e9 --- /dev/null +++ b/packages/opendesign/src/breadcrumb/__docs__/__case__/BcUsage.vue @@ -0,0 +1,24 @@ + + + +### 基础用法 + +通过 `href` 属性设置链接跳转地址,或者使用 `to` 属性配合 Vue Router 实现路由跳转。当 `href` 和 `to` 属性都未设置时,`OBreadcrumbItem` 组件会使用 `span` 标签渲染为一个普通的文本(这对于 SEO 是有好处的)。 + + + +### Basic Usage + +Set the link jump address using the `href` property, or use the `to` property in conjunction with Vue Router to achieve routing. When neither the `href` nor the `to` property is set, the `OBreadcrumbItem` component will render as a plain text using a `span` tag (which is beneficial for SEO). + + + + + diff --git a/packages/opendesign/src/breadcrumb/__docs__/__case__/BcVueRouter.vue b/packages/opendesign/src/breadcrumb/__docs__/__case__/BcVueRouter.vue new file mode 100644 index 0000000000000000000000000000000000000000..379f28ea79d3da9abddd274d93a7ebf8428b326f --- /dev/null +++ b/packages/opendesign/src/breadcrumb/__docs__/__case__/BcVueRouter.vue @@ -0,0 +1,54 @@ + + + +### 配合 Vue Router 使用 + +当给 `OBreadcrumbItem` 组件传入 `to` 属性时,它会使用`RouterLink`组件渲染。这样可以实现与 Vue Router 的集成,支持 SPA 应用跳转。 + +传入 `to` 属性时,`OBreadcrumbItem` 还可以使用 `replace` 属性来控制路由跳转时是否覆盖浏览器历史记录(`replace` 的值会直接传给 `RouterLink` 组件)。 + +需要注意在使用 `OBreadcrumbItem` 时已正确配置 Vue Router。 + +```js +import { createRouter, createWebHistory } from 'vue-router'; +import { createApp } from 'vue'; + +const router = createRouter({ + /** 省略 */ +}); +const app = createApp(/** 省略 */); +app.use(router); // 此处确保 OBreadcrumbItem 能正确解析RouterLink组件 +``` + + + +### Using with Vue Router + +When the `to` property is passed to the `OBreadcrumbItem` component, it will render using the `RouterLink` component. This allows integration with Vue Router, supporting SPA application navigation. + +When the `to` property is passed, the `OBreadcrumbItem` can also use the `replace` property to control whether the browser history is replaced during routing (the value of `replace` will be directly passed to the `RouterLink` component). + +It is important to ensure that Vue Router is correctly configured when using `OBreadcrumbItem`. + +```js +import { createRouter, createWebHistory } from 'vue-router'; +import { createApp } from 'vue'; + +const router = createRouter({ + /** omitted */ +}); +const app = createApp(/** omitted */); +app.use(router); // Ensure that OBreadcrumbItem can correctly parse the RouterLink component here +``` + + + + + diff --git a/packages/opendesign/src/breadcrumb/__docs__/index.en-US.md b/packages/opendesign/src/breadcrumb/__docs__/index.en-US.md new file mode 100644 index 0000000000000000000000000000000000000000..2f026ef24ca0cedee529ba17768b28fb1925889e --- /dev/null +++ b/packages/opendesign/src/breadcrumb/__docs__/index.en-US.md @@ -0,0 +1,16 @@ +--- +sidebar: OBreadcrumb +kind: nav +--- + +# Breadcrumb + +## Demo + + + + +## Api + + + diff --git a/packages/opendesign/src/breadcrumb/__docs__/index.zh-CN.md b/packages/opendesign/src/breadcrumb/__docs__/index.zh-CN.md new file mode 100644 index 0000000000000000000000000000000000000000..e5a0a4e203efb12560730d38dc1dd75905496558 --- /dev/null +++ b/packages/opendesign/src/breadcrumb/__docs__/index.zh-CN.md @@ -0,0 +1,16 @@ +--- +sidebar: OBreadcrumb 面包屑 +kind: nav +--- + +# 面包屑 + +## 示例 + + + + +## Api + + + diff --git a/packages/opendesign/src/breadcrumb/style/index.scss b/packages/opendesign/src/breadcrumb/style/index.scss index 504d59c44eea7243e3faa4e454b7675ea8b98768..8bb47c5840ef5591b0176e882570a9878c439b64 100644 --- a/packages/opendesign/src/breadcrumb/style/index.scss +++ b/packages/opendesign/src/breadcrumb/style/index.scss @@ -1,2 +1,3 @@ @use './style.scss' as *; +@use './var.scss' as *; @use './media.scss' as *; diff --git a/packages/opendesign/src/breadcrumb/style/media.scss b/packages/opendesign/src/breadcrumb/style/media.scss index e491a18a77ee77dead348d4cc23dae1740b352da..43264a37d473f028c8115cc9e152c565d1d21bd3 100644 --- a/packages/opendesign/src/breadcrumb/style/media.scss +++ b/packages/opendesign/src/breadcrumb/style/media.scss @@ -1,9 +1,9 @@ @use '../../_styles/mixin.scss' as *; -@include respond-to('<=laptop') { +@include respond-to('<=pc_s') { .o-breadcrumb { --breadcrumb-text-size: var(--o-font_size-tip2); --breadcrumb-text-height: var(--o-line_height-tip2); - --breadcrumb-seperator-size: var(--o-icon_size_control-xs); + --breadcrumb-separator-size: var(--o-icon_size_control-xs); } } diff --git a/packages/opendesign/src/breadcrumb/style/style.scss b/packages/opendesign/src/breadcrumb/style/style.scss index ec7bbeacfd622e2f8f696c7071419a58c8429e14..2d9da3a12a2268707c71512a97aaed7f68fbf3d2 100644 --- a/packages/opendesign/src/breadcrumb/style/style.scss +++ b/packages/opendesign/src/breadcrumb/style/style.scss @@ -1,5 +1,4 @@ @use '../../_styles/mixin.scss' as *; -@use './var.scss'; .o-breadcrumb { display: inline-flex; @@ -19,10 +18,10 @@ align-items: center; &:last-child { - color: var(--breadcrumb-color-selected); font-weight: 500; - + .o-breadcrumb-item-label { + color: var(--breadcrumb-color-selected); cursor: auto; max-width: none; } @@ -53,8 +52,8 @@ display: inline-flex; align-items: center; justify-content: center; - min-width: var(--breadcrumb-seperator-size); - font-size: var(--breadcrumb-seperator-size); + min-width: var(--breadcrumb-separator-size); + font-size: var(--breadcrumb-separator-size); line-height: 1; transition: color var(--o-duration-s) var(--o-easing-standard); margin: 0 var(--breadcrumb-gap); diff --git a/packages/opendesign/src/breadcrumb/style/theme-ascend.index.ts b/packages/opendesign/src/breadcrumb/style/theme-ascend.index.ts new file mode 100644 index 0000000000000000000000000000000000000000..5bdacd9c6ee5aca363917db515e154542266f4c2 --- /dev/null +++ b/packages/opendesign/src/breadcrumb/style/theme-ascend.index.ts @@ -0,0 +1,3 @@ +import '../../_styles'; +import './index.scss'; +import './theme-ascend.scss'; diff --git a/packages/opendesign/src/breadcrumb/style/theme-ascend.scss b/packages/opendesign/src/breadcrumb/style/theme-ascend.scss new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/packages/opendesign/src/breadcrumb/style/theme-kunpeng.index.ts b/packages/opendesign/src/breadcrumb/style/theme-kunpeng.index.ts new file mode 100644 index 0000000000000000000000000000000000000000..c2b4508ff081979f08fbcfda03f045c172570d33 --- /dev/null +++ b/packages/opendesign/src/breadcrumb/style/theme-kunpeng.index.ts @@ -0,0 +1,3 @@ +import '../../_styles'; +import './index.scss'; +import './theme-kunpeng.scss'; diff --git a/packages/opendesign/src/breadcrumb/style/theme-kunpeng.scss b/packages/opendesign/src/breadcrumb/style/theme-kunpeng.scss new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/packages/opendesign/src/breadcrumb/style/theme-openeuler.index.ts b/packages/opendesign/src/breadcrumb/style/theme-openeuler.index.ts new file mode 100644 index 0000000000000000000000000000000000000000..5a4690a05640253920c989242a44e2e2e5b4b720 --- /dev/null +++ b/packages/opendesign/src/breadcrumb/style/theme-openeuler.index.ts @@ -0,0 +1,3 @@ +import '../../_styles'; +import './index.scss'; +import './theme-openeuler.scss'; diff --git a/packages/opendesign/src/breadcrumb/style/theme-openeuler.scss b/packages/opendesign/src/breadcrumb/style/theme-openeuler.scss new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/packages/opendesign/src/breadcrumb/style/var.scss b/packages/opendesign/src/breadcrumb/style/var.scss index 2feaea59048e909fb07ce0038e9a70e4382d0386..7a558f533db68dc1fe81e8ca9db45e294e8c1c10 100644 --- a/packages/opendesign/src/breadcrumb/style/var.scss +++ b/packages/opendesign/src/breadcrumb/style/var.scss @@ -1,14 +1,14 @@ .o-breadcrumb { --breadcrumb-color: var(--o-color-info3); - --breadcrumb-color-hover: var(--o-color-info1); - --breadcrumb-color-active: var(--o-color-info1); - --breadcrumb-color-selected: var(--o-color-info1); + --breadcrumb-color-hover: var(--o-color-primary2); + --breadcrumb-color-active: var(--o-color-primary3); + --breadcrumb-color-selected: var(--o-color-primary1); --breadcrumb-text-size: var(--o-font_size-tip1); --breadcrumb-text-height: var(--o-line_height-tip1); --breadcrumb-gap: 4px; - --breadcrumb-seperator-size: var(--o-icon_size_control-m); + --breadcrumb-separator-size: var(--o-icon_size_control-m); --breadcrumb-label-max-width: 140px; } diff --git a/packages/opendesign/src/breadcrumb/types.ts b/packages/opendesign/src/breadcrumb/types.ts index 4f692d14413af0a5b82ce88c27fda4cecaeb6cfa..b74145a0e5b219bae9e7253a826e9dacbddfd5b8 100644 --- a/packages/opendesign/src/breadcrumb/types.ts +++ b/packages/opendesign/src/breadcrumb/types.ts @@ -2,7 +2,8 @@ import type { ExtractPropTypes, PropType } from 'vue'; export const breadcrumbProps = { /** - * 分隔符字符 + * @zh-CN 分隔符字符 + * @en-US Separator character */ separator: { type: [String, Number], @@ -11,33 +12,40 @@ export const breadcrumbProps = { export const breadcrumbItemProps = { /** - * 链接跳转地址 + * @zh-CN 链接跳转地址 + * @en-US Link jump address */ href: { type: String, }, /** - * 链接跳转方式 + * @zh-CN 链接跳转方式 + * @en-US Link jump method + * @default '_self' */ target: { type: String as PropType<'_blank' | '_parent' | '_self' | '_top'>, default: '_self', }, /** - * 路由跳转对象 + * @zh-CN 路由跳转对象。当使用该参数时,OBreadcrumbItem 会渲染为 RouterLink 组件 + * @en-US Route jump object. When using this parameter, OBreadcrumbItem will render as a RouterLink component */ to: { type: [String, Object] as PropType>, }, /** - * 路由跳转时,是否覆盖浏览器历史记录 + * @zh-CN 路由跳转时,是否覆盖浏览器历史记录。该参数会作为 RouterLink 的 replace 属性 + * @en-US Whether to replace the browser history when routing. This parameter will be used as the replace attribute of RouterLink + * @default false */ replace: { type: Boolean, default: false, }, /** - * 分隔符字符 + * @zh-CN 分隔符字符。会覆盖 OBreadcrumb 的 separator 属性 + * @en-US Separator character. This will override the separator property of OBreadcrumb */ separator: { type: [String, Number], diff --git a/packages/opendesign/src/button/__docs__/__case__/BtnAscendThemeOperation.vue b/packages/opendesign/src/button/__docs__/__case__/BtnAscendThemeOperation.vue new file mode 100644 index 0000000000000000000000000000000000000000..6b53988f661909e58a5428cac54bb6c7164b1ce4 --- /dev/null +++ b/packages/opendesign/src/button/__docs__/__case__/BtnAscendThemeOperation.vue @@ -0,0 +1,37 @@ + + + +### 昇腾主题规范 + +所有昇腾主题按钮需满足圆角为半圆的视觉规范,通过 `round="pill"` 参数统一设置圆角样式。 + +#### 按钮类型及配置说明 + +#### 运营按钮 + +- **功能定位**:用于运营活动相关的按钮交互 +- **配置方式**:为按钮组件添加类名 `c-btn-activity` 或 `c-btn-ascend`(二选一) + + + +### Ascend Theme Specifications + +All ascend-themed buttons must adhere to the visual specification of having semi-circular (half-pill) borders, with the border radius style uniformly configured via the `round="pill"` parameter. + +#### Button Types and Configuration Instructions + +#### Operational Buttons + +- **Functional Positioning**: Used for button interactions related to operational activities. +- **Configuration Method**: Add either the class name `c-btn-activity` or `c-btn-ascend` (choose one) to the button component. + + + diff --git a/packages/opendesign/src/button/__docs__/__case__/BtnAscendThemeText.vue b/packages/opendesign/src/button/__docs__/__case__/BtnAscendThemeText.vue new file mode 100644 index 0000000000000000000000000000000000000000..22e4bcf1349843c21adc40ec5a098617b78455c4 --- /dev/null +++ b/packages/opendesign/src/button/__docs__/__case__/BtnAscendThemeText.vue @@ -0,0 +1,54 @@ + + + +#### 文本按钮(无背景,无线框) + +- **基础组件**:使用 `OLink` 组件实现 +- **通用配置**:必须传递 `hover-underline` 参数(鼠标悬停时显示下划线) +- **特殊类型:箭头文本按钮** + 需额外完成以下配置: + - 添加类名 `c-link-ascend` + - 通过 `suffix` 插槽嵌入箭头图标,示例代码: + ```vue + + ``` + + + +#### Text Buttons (No Background, No Border) + +- **Base Component**: Implemented using the `OLink` component. +- **General Configuration**: Must pass the `hover-underline` parameter (displays an underline on mouse hover). +- **Special Type: Arrow Text Button**: + Requires additional configuration as follows: + - Add the class name `c-link-ascend`. + - Embed the arrow icon via the `suffix` slot. Example code: + ```vue + + ``` + + + diff --git a/packages/opendesign/src/button/__docs__/__case__/BtnKunpengThemeOperation.vue b/packages/opendesign/src/button/__docs__/__case__/BtnKunpengThemeOperation.vue new file mode 100644 index 0000000000000000000000000000000000000000..95170c12adbf9b86cdf11dc2f62e3809fdabaff5 --- /dev/null +++ b/packages/opendesign/src/button/__docs__/__case__/BtnKunpengThemeOperation.vue @@ -0,0 +1,37 @@ + + + +### 鲲鹏主题规范 + +所有鲲鹏主题按钮需满足圆角为半圆的视觉规范,通过 `round="pill"` 参数统一设置圆角样式。 + +#### 按钮类型及配置说明 + +#### 运营按钮 + +- **功能定位**:用于运营活动相关的按钮交互 +- **配置方式**:为按钮组件添加类名 `c-btn-activity` 或 `c-btn-kunpeng`(二选一) + + + +### Kunpeng Theme Specifications + +All Kunpeng-themed buttons must adhere to the visual specification of having semi-circular (half-pill) borders, with the border radius style uniformly configured via the `round="pill"` parameter. + +#### Button Types and Configuration Instructions + +#### Operational Buttons + +- **Functional Positioning**: Used for button interactions related to operational activities. +- **Configuration Method**: Add either the class name `c-btn-activity` or `c-btn-kunpeng` (choose one) to the button component. + + + diff --git a/packages/opendesign/src/button/__docs__/__case__/BtnKunpengThemeText.vue b/packages/opendesign/src/button/__docs__/__case__/BtnKunpengThemeText.vue new file mode 100644 index 0000000000000000000000000000000000000000..dbafbd68f7cab9260ec5dd517aed97bed60a4f30 --- /dev/null +++ b/packages/opendesign/src/button/__docs__/__case__/BtnKunpengThemeText.vue @@ -0,0 +1,28 @@ + + + +#### 文本按钮(无背景,无线框) + +- **基础组件**:使用 `OLink` 组件实现 +- **通用配置**:必须传递 `hover-underline` 参数(鼠标悬停时显示下划线) +- **hover特殊颜色**:因为鲲鹏主题要求文本按钮hover时文字变红并显示红色下划线,所以需要给 `OLink` 组件添加 `c-link-kunpeng` 类名 + + + +#### Text Buttons (No Background, No Border) + +- **Base Component**: Implemented using the `OLink` component. +- **General Configuration**: Must pass the `hover-underline` parameter (displays an underline on mouse hover). +- **Hover Special Color**: Because the Kunpeng theme requires text buttons to display red text and a red underline on hover, + the `OLink` component needs to have the `c-link-kunpeng` class name added. + + + + diff --git a/packages/opendesign/src/button/__docs__/__case__/BtnLoading.vue b/packages/opendesign/src/button/__docs__/__case__/BtnLoading.vue new file mode 100644 index 0000000000000000000000000000000000000000..67eb70468283886ffa4dc5baea956c447f6777f0 --- /dev/null +++ b/packages/opendesign/src/button/__docs__/__case__/BtnLoading.vue @@ -0,0 +1,36 @@ + + + +### 加载中 + +加载中的按钮,通过`loading`属性控制按钮的加载状态。当`loading`为`true`时,按钮会显示加载中的状态,同时`icon`插槽会被替换为加载中的图标。 + + + +### Loading + +A button that is loading, controlled by the `loading` property. When `loading` is `true`, the button will display the loading state. The `icon` slot will be replaced with the loading icon. + + + diff --git a/packages/opendesign/src/button/__docs__/__case__/BtnOpenEulerThemePrimary.vue b/packages/opendesign/src/button/__docs__/__case__/BtnOpenEulerThemePrimary.vue new file mode 100644 index 0000000000000000000000000000000000000000..4e3e95b2aef7ddcd92743da5451aa9edcd0b1de3 --- /dev/null +++ b/packages/opendesign/src/button/__docs__/__case__/BtnOpenEulerThemePrimary.vue @@ -0,0 +1,39 @@ + + + +### 欧拉主题规范 + +所有欧拉主题按钮需满足圆角为半圆的视觉规范,通过 `round="pill"` 参数统一设置圆角样式。 + +#### 按钮类型及配置说明 + +#### 主要按钮(主操作按钮) + +- **功能定位**:页面核心操作的主按钮(如提交、确认) +- **配置方式**:需同时传递两个参数 + `color="primary"`(标识颜色为主题色) + `variant="solid"`(实心样式) + + + +### Euler Theme Specifications + +All Euler-themed buttons must adhere to the visual specification of having semi-circular (half-pill) borders, with the border radius style uniformly configured via the `round="pill"` parameter. + +#### Button Types and Configuration Instructions + +#### Primary Buttons (Main Action Buttons) + +- **Functional Positioning**: Primary button for core page operations (e.g., submission, confirmation). +- **Configuration Method**: Requires passing two parameters simultaneously: + `color="primary"` (indicates the color as the theme color) + `variant="solid"` (solid style). + + + diff --git a/packages/opendesign/src/button/__docs__/__case__/BtnOpenEulerThemeText.vue b/packages/opendesign/src/button/__docs__/__case__/BtnOpenEulerThemeText.vue new file mode 100644 index 0000000000000000000000000000000000000000..00288e94391989f311e2ce0a472159c692820153 --- /dev/null +++ b/packages/opendesign/src/button/__docs__/__case__/BtnOpenEulerThemeText.vue @@ -0,0 +1,30 @@ + + + +#### 文本按钮(无背景,无线框) + +- **基础组件**:使用 `OLink` 组件实现 +- **颜色**:文本按钮的颜色有 `normal`、`primary`、`success`、`warning`、`danger` 5种,默认为 `normal` +- **注意**:欧拉主题中文本按钮无 hover 下划线,因此不能配置 `hover-underline` + + + +#### Text Buttons (No Background, No Border) + +- **Base Component**: Implemented using the `OLink` component. +- **Color Options**: Text buttons have 5 color variants: `normal`, `primary`, `success`, `warning`, and `danger`, with `normal` as the default. +- **Note**: In the Euler theme, text buttons do not support hover underlines, so the `hover-underline` parameter should not be configured. + + + diff --git a/packages/opendesign/src/button/__docs__/__case__/BtnRound.vue b/packages/opendesign/src/button/__docs__/__case__/BtnRound.vue new file mode 100644 index 0000000000000000000000000000000000000000..60ceae087b984534b94be923a4dd1f7ca1229165 --- /dev/null +++ b/packages/opendesign/src/button/__docs__/__case__/BtnRound.vue @@ -0,0 +1,59 @@ + + + +### 按钮圆角形状 + +通过`round`属性设置按钮圆角形状,值为`pill`时指半圆角,值也可以是css属性`border-radius`可接受的任意值 + + + +### Button Round Shape + +Sets the button's rounded shape. A value of 'pill' specifies a half-rounded shape. Any valid CSS border-radius length value can be used. + + + diff --git a/packages/opendesign/src/button/__docs__/__case__/BtnThemeIcon.vue b/packages/opendesign/src/button/__docs__/__case__/BtnThemeIcon.vue new file mode 100644 index 0000000000000000000000000000000000000000..645896fa1024e27af2e745dbf5b1b77708b993da --- /dev/null +++ b/packages/opendesign/src/button/__docs__/__case__/BtnThemeIcon.vue @@ -0,0 +1,34 @@ + + + +#### 图标按钮(仅含图标) + +- **基础组件**:使用 `OIcon` 组件实现 +- **必传参数**: + - `button`(标识组件为按钮类型) + - `:icon="iconName"`(动态绑定图标组件) +- **尺寸控制**:通过 `font-size` 属性调整按钮大小(支持内联样式或 CSS 类名方式) + + +#### Icon Buttons (Icon-only button) + +- **Base Component**: Implemented using the `OIcon` component. +- **Mandatory Parameters**: + - `button` (identifies the component as a button type). + - `:icon="iconName"` (dynamically binds the icon component). +- **Size Control**: Adjust the button size via the `font-size` property (supports inline styles or CSS class names). + + + + diff --git a/packages/opendesign/src/button/__docs__/__case__/BtnThemeLink.vue b/packages/opendesign/src/button/__docs__/__case__/BtnThemeLink.vue new file mode 100644 index 0000000000000000000000000000000000000000..80e87ed1b2203f077499a25cac9f031649b58601 --- /dev/null +++ b/packages/opendesign/src/button/__docs__/__case__/BtnThemeLink.vue @@ -0,0 +1,27 @@ + + + +#### 链接按钮(带链接功能的文本按钮) + +- **功能定位**:用于跳转外部/内部链接的按钮 +- **配置方式**: + 使用 `OLink` 组件,传递 `hover-underline`(悬停下划线) + `color="primary"`(链接主色样式)参数 + + + +#### Link Buttons (Text Buttons with Link Functionality) + +- **Functional Positioning**: Used for buttons that navigate to external/internal links. +- **Configuration Method**: + Use the `OLink` component, passing the `hover-underline` (hover underline) + `color="primary"` (link primary color style) parameters. + + + + diff --git a/packages/opendesign/src/button/__docs__/__case__/BtnThemeNormal.vue b/packages/opendesign/src/button/__docs__/__case__/BtnThemeNormal.vue new file mode 100644 index 0000000000000000000000000000000000000000..274d68e293f7d70bf4d5cb1a6dff6a207c9c1f84 --- /dev/null +++ b/packages/opendesign/src/button/__docs__/__case__/BtnThemeNormal.vue @@ -0,0 +1,27 @@ + + + +#### 普通按钮 + +- **功能定位**:辅助性操作按钮(如取消、返回) +- **配置方式**:需同时传递两个参数 + `color="primary"`(标识颜色为主题色) + `variant="outline"`(线框样式) + + + +#### Normal Buttons + +- **Functional Positioning**: Auxiliary action buttons (e.g., cancellation, return). +- **Configuration Method**: Requires passing two parameters simultaneously: + `color="primary"` (indicates the color as the theme color) + `variant="outline"` (outline style). + + + diff --git a/packages/opendesign/src/button/__docs__/__case__/BtnThemePrimary.vue b/packages/opendesign/src/button/__docs__/__case__/BtnThemePrimary.vue new file mode 100644 index 0000000000000000000000000000000000000000..d4ac29f1d76450dea645b4658f912b2d8e670bda --- /dev/null +++ b/packages/opendesign/src/button/__docs__/__case__/BtnThemePrimary.vue @@ -0,0 +1,26 @@ + + + +#### 强调按钮 + +- **功能定位**:页面核心操作的主按钮(如提交、确认) +- **配置方式**:需同时传递两个参数 +`color="primary"`(标识颜色为主题色) + `variant="solid"`(实心样式) + + +#### Primary Buttons (Main Action Buttons) + +- **Functional Positioning**: Primary button for core page operations (e.g., submission, confirmation). +- **Configuration Method**: Requires passing two parameters simultaneously: + `color="primary"` (indicates the color as the theme color) + `variant="solid"` (solid style). + + + diff --git a/packages/opendesign/src/button/__docs__/__case__/BtnUsage.vue b/packages/opendesign/src/button/__docs__/__case__/BtnUsage.vue new file mode 100644 index 0000000000000000000000000000000000000000..68d242071acdbf9865fc5a79205af6e76984cc1b --- /dev/null +++ b/packages/opendesign/src/button/__docs__/__case__/BtnUsage.vue @@ -0,0 +1,85 @@ + + + +### 使用 + +按钮包含 `primary`、`success`、`warning`、`danger` 四种主题色; + +三种尺寸:`small`、`medium`、`large` ; + +三种形状:`solid`、`outline`、`text` ; + +禁用状态:`disabled` ; + +加载状态:`loading` ; + +按钮的圆角可以通过 `pill` 设置为半圆,也可以是 css 属性 `border-radius` 能够接受的其它值。 + + + +### Usage + +The button includes four theme colors: `primary`, `success`, `warning`, and `danger`; + +Three sizes: `small`, `medium`, and `large`; + +Three shapes: `solid`, `outline`, and `text`; + +Disabled state: `disabled`; + +Loading state: `loading`; + +The button's radius can be set to half circle by setting `pill` to `true`, or to css property `border-radius` accepted values. + + diff --git a/packages/opendesign/src/button/__docs__/index.en-US.md b/packages/opendesign/src/button/__docs__/index.en-US.md new file mode 100644 index 0000000000000000000000000000000000000000..471bd999245cf9e3557c34d204c14b22c2dfa2eb --- /dev/null +++ b/packages/opendesign/src/button/__docs__/index.en-US.md @@ -0,0 +1,57 @@ +--- +sidebar: OButton +kind: operator +--- + +# Button + +## Demo + + + + + + + + + + + + + + + + + + + + + +## API + +### CSS Variables + +| CSS Variable | Description | +| ------------------------- | -------------------------------------------------------------------- | +| \-\-btn-color | Button text color | +| \-\-btn-color-hover | Button text color on hover | +| \-\-btn-color-active | Button text color when active | +| \-\-btn-bd-color | Button border color | +| \-\-btn-bd-color-hover | Button border color on hover | +| \-\-btn-bd-color-active | Button border color when active | +| \-\-btn-bd-color-disabled | Button border color in disabled state | +| \-\-btn-color-disabled | Button text color in disabled state | +| \-\-btn-bg-color | Button background color | +| \-\-btn-bg-color-hover | Button background color on hover | +| \-\-btn-bg-color-active | Button background color when active | +| \-\-btn-bg-color-disabled | Button background color in disabled state | +| \-\-btn-radius | Button border radius | +| \-\-btn-gap | Spacing between the button's preceding and following slots and text | +| \-\-btn-gap-prefix | Spacing between button prefix slot and text (defaults to `--btn-gap`) | +| \-\-btn-gap-suffix | Spacing between button suffix slot and text (defaults to `--btn-gap`) | +| \-\-btn-padding | Button internal padding | +| \-\-btn-icon-size | Size of text and icons in the button's preceding and following slots | +| \-\-btn-height | Button height | +| \-\-btn-min-width | Button minimum width (varies with the `size` parameter) | + + diff --git a/packages/opendesign/src/button/__docs__/index.zh-CN.md b/packages/opendesign/src/button/__docs__/index.zh-CN.md new file mode 100644 index 0000000000000000000000000000000000000000..6b7b8d806ce98491fa4ad5bfef85518a1fcab677 --- /dev/null +++ b/packages/opendesign/src/button/__docs__/index.zh-CN.md @@ -0,0 +1,57 @@ +--- +sidebar: OButton 按钮 +kind: operator +--- + +# 按钮 + +## 示例 + + + + + + + + + + + + + + + + + + + + + +## API + +### CSS 变量 + +| CSS 变量 | 描述 | +| ------------------------- | ------------------------------------ | +| \-\-btn-color | 按钮文字颜色 | +| \-\-btn-color-hover | 按钮鼠标悬停文字颜色 | +| \-\-btn-color-active | 按钮鼠标按下文字颜色 | +| \-\-btn-bd-color | 按钮边框颜色 | +| \-\-btn-bd-color-hover | 按钮鼠标悬停边框颜色 | +| \-\-btn-bd-color-active | 按钮鼠标按下边框颜色 | +| \-\-btn-bd-color-disabled | 按钮禁用状态边框颜色 | +| \-\-btn-color-disabled | 按钮禁用状态文字颜色 | +| \-\-btn-bg-color | 按钮背景色 | +| \-\-btn-bg-color-hover | 按钮鼠标悬停背景色 | +| \-\-btn-bg-color-active | 按钮鼠标按下背景色 | +| \-\-btn-bg-color-disabled | 按钮禁用状态背景色 | +| \-\-btn-radius | 按钮圆角 | +| \-\-btn-gap | 按钮前后插槽与文本间距 | +| \-\-btn-gap-prefix | 按钮前插槽与文本间距,默认值为 `--btn-gap` | +| \-\-btn-gap-suffix | 按钮后插槽与文本间距,默认值为 `--btn-gap` | +| \-\-btn-padding | 按钮内边距 | +| \-\-btn-icon-size | 按钮前后插槽文字及图标大小 | +| \-\-btn-height | 按钮高度 | +| \-\-btn-min-width | 按钮最小宽度(随着 `size` 参数变化) | + + diff --git a/packages/opendesign/src/button/style/index.scss b/packages/opendesign/src/button/style/index.scss index 504d59c44eea7243e3faa4e454b7675ea8b98768..8bb47c5840ef5591b0176e882570a9878c439b64 100644 --- a/packages/opendesign/src/button/style/index.scss +++ b/packages/opendesign/src/button/style/index.scss @@ -1,2 +1,3 @@ @use './style.scss' as *; +@use './var.scss' as *; @use './media.scss' as *; diff --git a/packages/opendesign/src/button/style/media.scss b/packages/opendesign/src/button/style/media.scss index 1d53a84060757c058a0391b16c697fe169c756b4..ee75a82af2da184ebc0870f4908744dae4fe98fc 100644 --- a/packages/opendesign/src/button/style/media.scss +++ b/packages/opendesign/src/button/style/media.scss @@ -1,11 +1,20 @@ @use '../../_styles/mixin.scss' as *; -@include respond-to('laptop') { +@include respond-to('pad_v-pc_s') { .o-btn-large { + --btn-height: 36px; --btn-padding: 0 15px; + font-size: var(--o-font_size-tip1); + line-height: var(--o-line_height-tip1); --btn-icon-size: var(--o-icon_size-s); - + } +} +@include respond-to('<=pc_s') { + .o-btn-medium { + --btn-height: 28px; + --btn-padding: 0 15px; font-size: var(--o-font_size-tip1); line-height: var(--o-line_height-tip1); + --btn-icon-size: var(--o-icon_size-xs); } } diff --git a/packages/opendesign/src/button/style/style.scss b/packages/opendesign/src/button/style/style.scss index a60079cb00f2a58bc304404f8f45feded84077ea..79d6720cc0cfbafd84232237b77fb341e69bed04 100644 --- a/packages/opendesign/src/button/style/style.scss +++ b/packages/opendesign/src/button/style/style.scss @@ -1,5 +1,4 @@ @use '../../_styles/mixin.scss' as *; -@use './var.scss'; .o-btn { outline: none; @@ -104,7 +103,7 @@ .o-btn-prefix { display: inline-flex; align-items: center; - margin-right: var(--btn-gap); + margin-right: var(--btn-gap-prefix); font-size: var(--btn-icon-size); .o-btn-icon-only & { @@ -117,6 +116,6 @@ .o-btn-suffix { display: inline-flex; align-items: center; - margin-left: var(--btn-gap); + margin-left: var(--btn-gap-suffix); font-size: var(--btn-icon-size); } diff --git a/packages/opendesign/src/button/style/theme-ascend.index.ts b/packages/opendesign/src/button/style/theme-ascend.index.ts new file mode 100644 index 0000000000000000000000000000000000000000..5bdacd9c6ee5aca363917db515e154542266f4c2 --- /dev/null +++ b/packages/opendesign/src/button/style/theme-ascend.index.ts @@ -0,0 +1,3 @@ +import '../../_styles'; +import './index.scss'; +import './theme-ascend.scss'; diff --git a/packages/portal-ak/src/ak/theme/button.scss b/packages/opendesign/src/button/style/theme-ascend.scss similarity index 97% rename from packages/portal-ak/src/ak/theme/button.scss rename to packages/opendesign/src/button/style/theme-ascend.scss index d8477354e604d3099bf757ed42b782390cce35da..63e91790243d968542416aa6ca558e281ca637d3 100644 --- a/packages/portal-ak/src/ak/theme/button.scss +++ b/packages/opendesign/src/button/style/theme-ascend.scss @@ -1,4 +1,4 @@ -@use './mixin.scss' as *; +@use '../../_styles/mixin.scss' as *; .o-btn.o-btn-solid { --btn-color: var(--o-color-info1-inverse); @@ -23,7 +23,6 @@ } .c-btn-activity, -.c-btn-kunpeng, .c-btn-ascend { color: var(--o-color-white) !important; background: linear-gradient(90deg, rgb(var(--ak-color-band-start)) 0%, rgb(var(--ak-color-band-end)) 100%); diff --git a/packages/opendesign/src/button/style/theme-kunpeng.index.ts b/packages/opendesign/src/button/style/theme-kunpeng.index.ts new file mode 100644 index 0000000000000000000000000000000000000000..c2b4508ff081979f08fbcfda03f045c172570d33 --- /dev/null +++ b/packages/opendesign/src/button/style/theme-kunpeng.index.ts @@ -0,0 +1,3 @@ +import '../../_styles'; +import './index.scss'; +import './theme-kunpeng.scss'; diff --git a/packages/opendesign/src/button/style/theme-kunpeng.scss b/packages/opendesign/src/button/style/theme-kunpeng.scss new file mode 100644 index 0000000000000000000000000000000000000000..a1cbbcc0c1e42c2d57f0b172d9893264b52895f3 --- /dev/null +++ b/packages/opendesign/src/button/style/theme-kunpeng.scss @@ -0,0 +1,47 @@ +@use '../../_styles/mixin.scss' as *; + +.o-btn.o-btn-solid { + --btn-color: var(--o-color-info1-inverse); + --btn-color-hover: var(--o-color-info1-inverse); + --btn-color-active: var(--o-color-info1-inverse); + --btn-color-disabled: var(--o-color-info1-inverse); +} + +.o-btn-outline.o-btn-primary:not(.o-btn-disabled) { + --btn-color: var(--o-color-info1); + --btn-color-hover: var(--o-color-info1-inverse); + --btn-color-active: var(--o-color-info1-inverse); + --btn-bg-color-hover: var(--o-color-primary2); + --btn-bg-color-active: var(--o-color-primary3); + @include hover { + background-color: var(--btn-bg-color-hover); + } + + &:active { + background-color: var(--btn-bg-color-active); + } +} + +.c-btn-activity, +.c-btn-kunpeng { + color: var(--o-color-white) !important; + background: linear-gradient(90deg, rgb(var(--ak-color-band-start)) 0%, rgb(var(--ak-color-band-end)) 100%); + border-color: transparent; + background-repeat: no-repeat; + border: none; + @include hover { + background: linear-gradient(90deg, rgba(var(--ak-color-band-start), 0.8) 0%, rgba(var(--ak-color-band-end), 0.8) 100%); + border-color: transparent; + border: none; + } + &:active { + background: linear-gradient(90deg, rgba(var(--ak-color-band-start), 0.9) 0%, rgba(var(--ak-color-band-end), 0.9) 100%); + border-color: transparent; + border: none; + } + &.o-btn-disabled { + opacity: 0.4; + background: linear-gradient(90deg, rgb(var(--ak-color-band-start)) 0%, rgb(var(--ak-color-band-end)) 100%); + border: none; + } +} diff --git a/packages/opendesign/src/button/style/theme-openeuler.index.ts b/packages/opendesign/src/button/style/theme-openeuler.index.ts new file mode 100644 index 0000000000000000000000000000000000000000..5a4690a05640253920c989242a44e2e2e5b4b720 --- /dev/null +++ b/packages/opendesign/src/button/style/theme-openeuler.index.ts @@ -0,0 +1,3 @@ +import '../../_styles'; +import './index.scss'; +import './theme-openeuler.scss'; diff --git a/packages/opendesign/src/button/style/theme-openeuler.scss b/packages/opendesign/src/button/style/theme-openeuler.scss new file mode 100644 index 0000000000000000000000000000000000000000..be1cb63cffbf406841fec4a9ab6d75d54286e89d --- /dev/null +++ b/packages/opendesign/src/button/style/theme-openeuler.scss @@ -0,0 +1,23 @@ +@use '../../_styles/mixin.scss' as *; + +.o-btn.o-btn-solid { + --btn-color: var(--o-color-info1-inverse); + --btn-color-hover: var(--o-color-info1-inverse); + --btn-color-active: var(--o-color-info1-inverse); + --btn-color-disabled: var(--o-color-info1-inverse); +} + +.o-btn-outline.o-btn-primary:not(.o-btn-disabled) { + --btn-color: var(--o-color-info1); + --btn-color-hover: var(--o-color-info1-inverse); + --btn-color-active: var(--o-color-info1-inverse); + --btn-bg-color-hover: var(--o-color-primary2); + --btn-bg-color-active: var(--o-color-primary3); + @include hover { + background-color: var(--btn-bg-color-hover); + } + + &:active { + background-color: var(--btn-bg-color-active); + } +} diff --git a/packages/opendesign/src/button/style/var.scss b/packages/opendesign/src/button/style/var.scss index 49e2c789430adf97195166c5b73c81bd36090dba..a30c3c1bb60e0a473ed89180eb19dad4a5a06cd5 100644 --- a/packages/opendesign/src/button/style/var.scss +++ b/packages/opendesign/src/button/style/var.scss @@ -158,6 +158,8 @@ .o-btn-small { --btn-radius: var(--o-radius_control-xs); --btn-gap: 2px; + --btn-gap-prefix: var(--btn-gap); + --btn-gap-suffix: var(--btn-gap); --btn-padding: 0 11px; --btn-icon-size: var(--o-icon_size-xs); --btn-height: var(--o-control_size-s); @@ -167,6 +169,8 @@ --btn-radius: var(--o-radius_control-s); --btn-gap: 8px; + --btn-gap-prefix: var(--btn-gap); + --btn-gap-suffix: var(--btn-gap); --btn-padding: 0 15px; --btn-icon-size: var(--o-icon_size-xs); --btn-height: var(--o-control_size-m); @@ -176,6 +180,8 @@ --btn-radius: var(--o-radius_control-s); --btn-gap: 8px; + --btn-gap-prefix: var(--btn-gap); + --btn-gap-suffix: var(--btn-gap); --btn-padding: 0 23px; --btn-icon-size: var(--o-icon_size-m); --btn-height: var(--o-control_size-l); @@ -185,7 +191,6 @@ } .o-btn-round-pill { --btn-radius: var(--o-control_size-l); - --btn-gap: 6px; } .o-btn-text { diff --git a/packages/opendesign/src/button/types.ts b/packages/opendesign/src/button/types.ts index 4787b59e45db05b8bbea667c7f4a51d0328c8ee7..b27eb411eb2abf22702a1377d6f63333d2b2ada9 100644 --- a/packages/opendesign/src/button/types.ts +++ b/packages/opendesign/src/button/types.ts @@ -6,57 +6,69 @@ export type ButtonSizeT = (typeof ButtonSizeTypes)[number]; export const buttonProps = { /** - * 颜色类型 ColorT + * @zh-CN 颜色类型 + * @en-US Color type + * @default 'normal' */ color: { type: String as PropType, default: 'normal', }, /** - * 按钮类型 VariantT + * @zh-CN 按钮类型 + * @en-US Button type + * @default 'outline' */ variant: { type: String as PropType, default: 'outline', }, /** - * 按钮尺寸 ButtonSizeT + * @zh-CN 按钮尺寸 + * @en-US Button size */ size: { type: String as PropType, }, /** - * 圆角值 RoundT + * @zh-CN 圆角值 + * @en-US Border radius */ round: { type: String as PropType, }, /** - * 是否为loading状态 + * @zh-CN 是否为加载状态 + * @en-US Loading state */ loading: { type: Boolean, }, /** - * 是否为禁用状态 + * @zh-CN 是否禁用 + * @en-US Disabled state */ disabled: { type: Boolean, }, /** - * 链接跳转 + * @zh-CN 跳转链接,如果设置了此属性,则按钮会以 a 标签渲染 + * @en-US Link to navigate, if set, the button will render as an anchor tag */ href: { type: String, }, /** - * 前缀图标 + * @zh-CN 前缀图标 + * @en-US Prefix icon */ icon: { type: Object as PropType, }, /** - * 自定义元素标签 + * @zh-CN 自定义按钮渲染标签 + * @en-US Custom button render tag + * @default 'button' */ tag: { type: String, diff --git a/packages/opendesign/src/card/OCard.vue b/packages/opendesign/src/card/OCard.vue index bbfcf4ff27cbb1f47f2962f28e95e4995f77c669..95ab9390b766d051cb766dfe4ce493c3f6194ea1 100644 --- a/packages/opendesign/src/card/OCard.vue +++ b/packages/opendesign/src/card/OCard.vue @@ -29,6 +29,10 @@ const isTitleLimited = computed(() => { const isDetailLimited = computed(() => { return !isUndefined(props.detailMaxRow); }); + +const hasCover = computed(() => { + return Boolean(slots.cover || props.cover); +});